Core Concepts
This guide explains the building blocks of functional-examples: what an example is, how extractors discover them, how plugins compose behavior, and how the scanner ties it all together.
Examples
An Example is the fundamental unit. It represents a single scannable code example with:
- id — unique identifier (e.g.,
"basic-usage") - title — human-readable name
- description — what the example demonstrates
- files — the actual source files belonging to this example
- metadata — arbitrary key-value data (tags, difficulty, category, etc.)
- extractorName — which extractor discovered this example
Every plugin ultimately produces Example objects. The scanner collects them all into a uniform ScanResult.
Extractors
An Extractor is a function that receives a list of file candidates and returns examples. It's the core mechanism plugins use to discover examples in your project tree.
The extractor contract:
- Receive
Dirent[]candidates (files matching the scan patterns) - Inspect files, parse metadata, group related files
- Return an
ExtractorResultwith:examples— the discovered exampleserrors— any problems encounteredclaimedFiles— paths this extractor "owns" (prevents double-extraction)
Here's a custom extractor that parses 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;
}
The claimed files mechanism is important: when multiple plugins are active, each extractor claims the files it processes. This prevents the same file from being extracted twice by different plugins.
Plugins
A Plugin is a container that provides one or more capabilities:
| Capability | Purpose |
|---|---|
| extractor | Discovers examples from files |
| validators | Checks example metadata against rules |
| schemas | JSON Schema definitions for metadata and options |
| commands | CLI commands the plugin adds |
| extensions | File extensions the plugin handles |
Plugins are registered in your config:
import { createJavaScriptPlugin } from '@functional-examples/javascript';
import { createTestPlugin } from '@functional-examples/test';
import { createDocumentationPlugin } from '@functional-examples/documentation';
export default {
plugins: [
createJavaScriptPlugin(), // extracts from frontmatter or package.json
createTestPlugin(), // reads metadata.test for assertions
createDocumentationPlugin(), // adds doc generation
],
};
Each plugin operates independently. The scanner calls each plugin's extractor in sequence, passing unclaimed files to subsequent extractors.
Regions
Regions (also called hunks) are named sections within a file, marked with #region / #endregion comments:
/**
* ---
* id: region-markers
* title: Region Markers Demo
* ---
*/
import { scan } from 'functional-examples';
// #region setup
const result = await scan();
// #endregion
// #region execution
console.log(result.examples);
// #endregion
Regions let you reference specific parts of a file in documentation. Instead of showing an entire file, you can pull just the setup region:
<%= example('my-example').region('setup') %\>;
Note
There's a zero width space in the above code to prevent it from being processed by eta, copying it directly will not work.
The JavaScript plugin automatically detects #region / #endregion markers and records their line ranges as hunks on the file.
The Scanner Pipeline
When you call scan() or scanExamples(), here's what happens:
- Config resolution — the config file is loaded, plugins are instantiated, and scan patterns are resolved
- File discovery — the scanner globs for files matching
scan.includepatterns (minusscan.exclude) - Path mapping — if
pathMappingsare configured, files are routed to specific extractors by pattern - Extraction — each plugin's extractor runs against its candidate files, producing examples and claiming files
- Validation — if the config includes a
metadataschema, each example's metadata is validated against it - Result assembly — examples, errors, and stats are collected into a
ScanResult
The result gives you everything you need to generate docs, run tests, or build custom tooling.
Configuration
Configuration lives in a functional-examples.config.ts (or .json) file at your project root. It controls:
- plugins — which plugins to load
- scan.root — base directory for scanning
- scan.include / scan.exclude — glob patterns for file discovery
- pathMappings — route specific file patterns to specific extractors
- metadata — JSON Schema for validating example metadata
See the Configuration guide for the full reference.