DOCS/Plugins

Plugins

Plugins are the primary extension mechanism in functional-examples. Each plugin provides extractors, validators, commands, or schemas. This guide covers the built-in plugins, when to reach for each one, and how to compose them.

JavaScript Plugin

Package: @functional-examples/javascript

The JavaScript plugin extracts examples from .ts, .js, .tsx, .jsx, .mjs, .cjs, .mts, and .cts files. It supports two extraction modes:

Single-file (frontmatter): Individual source files with YAML frontmatter in comment blocks. The frontmatter provides id, title, and any custom metadata. The plugin also detects #region / #endregion markers for referencing specific code sections.

Here's a single-file example:

// ---
// id: getting-started
// title: Getting Started
// description: A simple example demonstrating frontmatter metadata extraction
// tags:
//   - beginner
//   - tutorial
// ---

/**
 * A simple greeting function.
 *
 * @param name - The name to greet
 * @returns A greeting message
 */
export function greet(name: string): string {
  return `Hello, ${name}!`;
}

// Example usage of the greet function
const message = greet('World');
console.log(message); // Output: Hello, World!

Multi-file (package.json): Directories with a package.json that provides metadata. The plugin reads name (for id), description, keywords, and custom fields from the functional-examples key. It traces entry points (main, module, exports) to discover which files belong to the example.

When to use it:

  • Your examples are TypeScript or JavaScript (single or multi-file)
  • You want metadata co-located with the code
  • You want region markers for referencing specific code sections
  • You want multi-file examples with automatic entry point tracing

Full guide → JavaScript Plugin — frontmatter syntax, region markers, package.json mode, and all configuration options.

YAML Manifest Plugin

Package: @functional-examples/yaml-manifest

The YAML manifest plugin discovers examples via meta.yml files in directories. Each meta.yml declares the example's metadata, and all sibling files become part of the example.

When to use it:

  • Your examples span multiple files
  • You work with non-JS/TS files (Python, Go, Rust, etc.)
  • You want metadata separate from source code
  • You need complex metadata structures

Full guide → YAML Manifest Plugin — meta.yml field reference, directory discovery, and when to choose YAML vs JavaScript.

Test Plugin

Package: @functional-examples/test

The test plugin adds testing capabilities. It reads test definitions from metadata.test on any scanned example — regardless of which extractor produced it. The test plugin doesn't extract examples itself; it operates on metadata populated by other plugins.

For JavaScript plugin examples using package.json, test definitions live under the functional-examples.test key:

{
  "name": "@examples/test-plugin-example",
  "private": true,
  "type": "module",
  "description": "Demonstrates the test plugin with command execution and assertions",
  "dependencies": {
    "functional-examples": "workspace:*",
    "@functional-examples/javascript": "workspace:*",
    "@functional-examples/test": "workspace:*"
  },
  "functional-examples": {
    "title": "Test Plugin Example",
    "tags": ["testing", "assertions", "commands"]
  }
}

For frontmatter-based examples, you could add a test field to the YAML metadata. For YAML manifest examples, add it to meta.yml. The test plugin doesn't care where the metadata came from — it just validates and runs whatever it finds in metadata.test.

Run tests with:

npx functional-examples test

When to use it:

  • You want CI verification that examples actually run
  • You need to assert exit codes, stdout, or stderr patterns
  • You want regression testing for example code

Full guide → Test Plugin — full assertion reference, multi-step tests, snapshots, and reporter options.

Documentation Plugin

Package: @functional-examples/documentation

The documentation plugin generates markdown documentation from scanned examples. It provides:

  • Template-based doc generation (functional-examples generate)
  • Prose rendering (expanding file() and region() references in README.md files)
  • Guide hydration (expanding cross-example references in standalone guides)

When to use it:

  • You want auto-generated API docs from example code
  • You write guides that reference live example files
  • You need consistent documentation format across many examples

Full guide → Documentation Plugin — template system, prose helpers, and guide rendering.

Multi-Plugin Setup

You can combine multiple plugins. When doing so, use pathMappings to route files to the right extractor and avoid conflicts:

{
  "$schema": "./.functional-examples/schema.json",
  "scan": {
    "include": ["src/**/*", "tutorials/*"],
    "exclude": ["**/node_modules/**"]
  },
  "pathMappings": [
    {
      "pattern": "src/**",
      "extractor": "javascript-extractor"
    },
    {
      "pattern": "tutorials/**",
      "extractor": "meta-yml"
    }
  ]
}

Path mappings use glob patterns to assign files to specific extractors by name. Files that match a mapping are only sent to the specified extractor. Unmapped files are sent to all extractors.

Custom Extractors

When the built-in plugins don't fit your metadata format, write a custom extractor. An extractor is a function that receives file candidates and returns examples.

Here's a complete custom extractor that reads TOML metadata files:

/**
 * Custom TOML-based extractor example.
 *
 * This demonstrates how to create your own extractor that:
 * 1. Scans for a specific file pattern (meta.toml)
 * 2. Parses metadata from that file format
 * 3. Claims files and returns Example objects
 */
import {
  ExampleFile,
  type Example,
  type Extractor,
  type ExtractorResult,
} from 'functional-examples';
import { readdirSync, type Dirent } from 'node:fs';
import { readFile } from 'node:fs/promises';
import path, { join } from 'node:path';

/**
 * Metadata structure for TOML examples.
 * You can define any shape that fits your use case.
 */
export interface TomlMetadata {
  id: string;
  title: string;
  description?: string;
  author?: string;
  [key: string]: unknown;
}

/**
 * Create a custom extractor that reads TOML metadata files.
 *
 * Extractors implement a candidate-based pattern: they're called with
 * pre-filtered candidates (files and directories) and decide which to handle.
 */
export function createTomlExtractor(): Extractor<TomlMetadata> {
  return {
    name: 'toml-extractor',

    async extract(
      candidates: Dirent[]
    ): Promise<ExtractorResult<TomlMetadata>> {
      const examples: Example<TomlMetadata>[] = [];
      const claimedFiles = new Set<string>();
      const errors: { path: string; message: string }[] = [];

      // Find meta.toml files from candidates
      const tomlFiles: string[] = [];

      for (const candidate of candidates) {
        const fullPath = path.join(candidate.parentPath, candidate.name);

        if (candidate.isFile()) {
          // Direct file candidate: check if it's a meta.toml
          if (candidate.name === 'meta.toml') {
            tomlFiles.push(fullPath);
          }
        } else if (candidate.isDirectory()) {
          // Directory candidate: look for meta.toml inside
          const metaPath = path.join(fullPath, 'meta.toml');
          try {
            await readFile(metaPath, 'utf-8');
            tomlFiles.push(metaPath);
          } catch {
            // No meta.toml in this directory, skip
          }
        }
      }

      for (const tomlFile of tomlFiles) {
        try {
          const content = await readFile(tomlFile, 'utf-8');
          const metadata = parseSimpleToml(content);

          const exampleDir = path.dirname(tomlFile);

          // Collect all files in the example directory
          const files = collectExampleFiles(exampleDir);

          // Claim all files
          for (const file of files) {
            claimedFiles.add(file);
          }

          examples.push({
            id: metadata.id,
            title: metadata.title,
            description: metadata.description,
            rootPath: exampleDir,
            files: files.map((f) => new ExampleFile({
              absolutePath: f,
              relativePath: path.relative(exampleDir, f),
            })),
            metadata,
            extractorName: 'toml-extractor',
          });
        } catch (err) {
          errors.push({
            path: tomlFile,
            message: `Failed to parse: ${(err as Error).message}`,
          });
        }
      }

      return { examples, errors, claimedFiles };
    },
  };
}

/**
 * Simplified TOML parser for demonstration.
 * In production, use a proper TOML library like @iarna/toml.
 */
function parseSimpleToml(content: string): TomlMetadata {
  const lines = content.split('\n');
  const result: Record<string, string> = {};

  for (const line of lines) {
    // Match: key = "value"
    const match = line.match(/^(\w+)\s*=\s*"(.*)"/);
    if (match) {
      result[match[1]] = match[2];
    }
  }

  if (!result['id'] || !result['title']) {
    throw new Error('TOML must have id and title fields');
  }

  return {
    id: result['id'],
    title: result['title'],
    description: result['description'],
    author: result['author'],
  };
}

function collectExampleFiles(root: string) {
  if (root.endsWith('node_modules')) {
    return [];
  }

  let files: string[] = [];
  const entries = readdirSync(root, { withFileTypes: true });
  for (const entry of entries) {
    if (entry.isDirectory()) {
      files = files.concat(
        collectExampleFiles(join(entry.parentPath, entry.name))
      );
    } else {
      files.push(join(entry.parentPath, entry.name));
    }
  }
  return files;
}

And here's how to use it in a scan script:

/**
 * Scan script using the custom TOML extractor.
 */
import { scan } from 'functional-examples';
import { resolveConfig } from 'functional-examples';
import { createTomlExtractor } from './toml-extractor.js';

async function main() {
  const jsonOutput = process.argv.includes('--json');

  const config = await resolveConfig({
    root: '.',
    plugins: [
      {
        name: 'toml-extractor',
        extractor: createTomlExtractor(),
      },
    ],
    scan: {
      include: ['**/*'],
      exclude: ['**/node_modules/**'],
    },
  });

  const result = await scan({ config });

  if (jsonOutput) {
    console.log(
      JSON.stringify(
        {
          examples: result.examples.map((e) => ({
            id: e.id,
            title: e.title,
            description: e.description,
            files: e.files.map((f) => f.relativePath),
            metadata: e.metadata,
          })),
          errors: result.errors,
          stats: result.stats,
        },
        null,
        2
      )
    );
  } else {
    console.log(`Found ${result.examples.length} TOML example(s):\n`);
    for (const example of result.examples) {
      console.log(`  ${example.id}: ${example.title}`);
      if (example.description) {
        console.log(`    ${example.description}`);
      }
      console.log(
        `    Files: ${example.files.map((f) => f.relativePath).join(', ')}`
      );
      console.log();
    }

    if (result.errors.length > 0) {
      console.log(`Errors (${result.errors.length}):`);
      for (const error of result.errors) {
        console.log(`  - ${error.path}: ${error.message}`);
      }
    }
  }
}

main().catch(console.error);

Key points for custom extractors:

  • Return claimedFiles so other extractors don't process the same files
  • Return errors for recoverable issues (the scanner collects them)
  • Throw for unrecoverable failures
  • Use extractorName to identify your extractor in results