@asafarim/md-file-explorer
v1.2.4
Published
A TypeScript package for recursively exploring markdown files and folders with lazy loading capabilities
Downloads
743
Maintainers
Readme
@asafarim/md-file-explorer
A TypeScript library for recursively exploring markdown files and folders with lazy loading, file watching, full-text search, and metadata parsing.
Live Demo · Documentation · Report Bug · Request Feature
✨ Features
- 🚀 Lazy Loading — Scan directories on-demand instead of loading everything upfront
- 📁 Recursive Scanning — Automatically discovers nested folders and markdown files
- 🔍 Smart Filtering — Include or exclude files based on extensions and glob patterns
- 📝 Metadata Parsing — Extracts YAML frontmatter from markdown files automatically
- 👀 File Watching — Get real-time notifications when files are added, changed, or removed
- 🔎 Full-Text Search — Search by filename or file content across your entire tree
- 🎯 Fully Typed — Written in TypeScript with comprehensive type definitions
- ⚡ High Performance — Only loads content when needed, with configurable depth limits
📦 Installation
# npm
npm install @asafarim/md-file-explorer
# yarn
yarn add @asafarim/md-file-explorer
# pnpm
pnpm add @asafarim/md-file-explorer🚀 Quick Start
import { MdFileExplorer } from '@asafarim/md-file-explorer';
// Create an explorer instance
const explorer = new MdFileExplorer('/path/to/docs', {
includeExtensions: ['.md', '.mdx'],
excludePatterns: ['node_modules', '.git'],
parseMarkdownMetadata: true,
});
// Get the file tree (lazy-loaded by default)
const fileTree = await explorer.getFileTree();
// Read a specific file's content
const content = await explorer.getFileContent('guides/getting-started.md');
// Scan an entire directory at once
const scanResult = await explorer.scanDirectory();📖 API Reference
MdFileExplorer
The main class for exploring markdown files.
Constructor
new MdFileExplorer(rootPath: string, options?: ExplorerOptions)| Parameter | Type | Description |
| ----------- | ----------------- | ------------------------------------ |
| rootPath | string | Root directory to explore |
| options | ExplorerOptions | Optional configuration (see below) |
Methods
| Method | Returns | Description |
| ----------------------------------- | ------------------------ | -------------------------------------------- |
| scanDirectory(path?, options?) | Promise<ScanResult> | Scan a directory and return the complete tree |
| getFileTree(path?, depth?) | Promise<FileNode[]> | Get the file tree with an optional depth limit |
| getFileContent(filePath) | Promise<FileContent> | Get the content of a specific file |
| watchDirectory(callback) | void | Watch for file system changes in real-time |
| stopWatching() | void | Stop watching for file changes |
| searchFiles(query, searchInContent?) | Promise<FileNode[]> | Search for files by name or content |
| searchFilesDetailed(query, searchInContent?) | Promise<SearchResult[]> | Search and return match type + snippet |
| fileExists(filePath) | Promise<boolean> | Check if a file exists in the tree |
Types
FileNode
interface FileNode {
name: string; // File or folder name
path: string; // Relative path from root
type: 'folder' | 'file'; // Node type
children?: FileNode[]; // Child nodes (folders only)
lastModified?: Date; // Last modification date
size?: number; // File size in bytes
metadata?: MarkdownMetadata; // Parsed frontmatter (if enabled)
}ExplorerOptions
interface ExplorerOptions {
rootPath: string; // Root directory path
includeExtensions?: string[]; // e.g. ['.md', '.mdx']
excludePatterns?: string[]; // e.g. ['node_modules', '.git']
maxDepth?: number; // Maximum scan depth (default: unlimited)
sortBy?: 'name' | 'date' | 'size'; // Sort criteria (default: 'name')
sortOrder?: 'asc' | 'desc'; // Sort direction (default: 'asc')
includeDotFiles?: boolean; // Include hidden files (default: false)
parseMarkdownMetadata?: boolean; // Parse YAML frontmatter (default: false)
}MarkdownMetadata
interface MarkdownMetadata {
title?: string;
description?: string;
tags?: string[];
[key: string]: unknown; // Any additional frontmatter fields
}ScanResult
interface ScanResult {
rootPath: string;
nodes: FileNode[];
totalFiles: number;
totalFolders: number;
totalSize: number;
}SearchResult
interface SearchResult {
node: FileNode; // The matched file node
matchType: 'name' | 'metadata' | 'content'; // Where the match was found
snippet?: string; // Excerpt around the match (content matches only)
}Utilities
The package also exports these helper functions:
import {
normalizePath,
isValidMarkdownFile,
shouldExcludePath,
getFileStats,
parseMarkdownMetadata,
sortFileNodes,
createFileNode,
getRelativePath,
} from '@asafarim/md-file-explorer';💡 Examples
Lazy Loading with Depth Control
Only load what you need — great for large documentation trees:
const explorer = new MdFileExplorer('/docs');
// Get only top-level folders and files
const topLevel = await explorer.getFileTree('/', 1);
// Load contents of a specific folder (2 levels deep)
const packageDocs = await explorer.getFileTree('/packages', 2);Real-Time File Watching
React to file changes as they happen:
explorer.watchDirectory((event, path) => {
console.log(`File ${event}: ${path}`);
// Events: 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir'
});
// Stop watching when done
explorer.stopWatching();Searching Files
Find files by name, metadata, or search inside their content:
// Search by filename only — fast
const byName = await explorer.searchFiles('react');
// Search inside file contents too — thorough
const byContent = await explorer.searchFiles('useState', true);
// Get detailed results with match type and snippets
const detailed = await explorer.searchFilesDetailed('hooks', true);
detailed.forEach(result => {
console.log(result.matchType); // 'name' | 'metadata' | 'content'
console.log(result.node.path); // file path
console.log(result.snippet); // excerpt around the match (content matches)
});Custom Configuration
Fine-tune the explorer for your project:
const explorer = new MdFileExplorer('/docs', {
includeExtensions: ['.md', '.mdx', '.txt'],
excludePatterns: ['draft-*', 'temp/**', 'private/'],
maxDepth: 5,
sortBy: 'date',
sortOrder: 'desc',
includeDotFiles: false,
parseMarkdownMetadata: true,
});🔗 Integration Examples
Express API Server
import express from 'express';
import { MdFileExplorer } from '@asafarim/md-file-explorer';
const app = express();
const explorer = new MdFileExplorer('./docs');
// Serve the file tree
app.get('/api/docs/tree', async (req, res) => {
const { path, depth } = req.query;
const tree = await explorer.getFileTree(path as string, Number(depth) || 1);
res.json(tree);
});
// Serve file content
app.get('/api/docs/file', async (req, res) => {
const { path } = req.query;
try {
const content = await explorer.getFileContent(path as string);
res.json(content);
} catch {
res.status(404).json({ error: 'File not found' });
}
});
// Search files (returns detailed results with match type and snippets)
app.get('/api/docs/search', async (req, res) => {
const { q, content } = req.query;
const results = await explorer.searchFilesDetailed(q as string, content === 'true');
res.json(results);
});
app.listen(3000);React Component
import React, { useState, useEffect } from 'react';
import { MdFileExplorer, FileNode } from '@asafarim/md-file-explorer';
function DocumentationExplorer() {
const [explorer] = useState(() => new MdFileExplorer('./docs'));
const [fileTree, setFileTree] = useState<FileNode[]>([]);
useEffect(() => {
const loadTree = async () => {
const tree = await explorer.getFileTree();
setFileTree(tree);
};
loadTree();
// Auto-refresh on file changes
explorer.watchDirectory(() => loadTree());
return () => explorer.stopWatching();
}, [explorer]);
return (
<ul>
{fileTree.map(node => (
<li key={node.path}>{node.name}</li>
))}
</ul>
);
}📂 Recommended Project Structure
The package works best with organized documentation:
docs/
├── README.md
├── guides/
│ ├── getting-started.md
│ └── advanced-usage.md
├── api/
│ ├── overview.md
│ └── reference/
│ ├── classes.md
│ └── interfaces.md
└── examples/
└── basic-usage.md⚡ Performance Tips
- Use depth limits — Pass a
depthargument togetFileTree()to avoid scanning huge trees - Exclude wisely — Always exclude
node_modules,.git, and build output directories - Watch sparingly — Only enable file watching in development or preview environments
- Cache content — Cache results of
getFileContent()for frequently accessed files - Sort strategically — Sorting by
dateis useful for blogs;nameis best for docs - Content search is slower —
searchFiles(query, true)reads every file; use it only when filename search isn't enough
🛠️ Development
# Install dependencies
pnpm install
# Build the library
pnpm build
# Run the demo locally (builds library first, then starts Vite dev server on port 3106)
pnpm dev
# Run tests
pnpm test
# Publish to npm (bumps version first, then publishes and pushes tags)
npm version minor && pnpm publishThe demo app includes a dark mode that automatically follows your system's prefers-color-scheme setting, with all colors driven by CSS custom properties for seamless light/dark switching.
🤝 Contributing
Contributions are welcome! Here's how to get started:
- Fork the repository
- Clone your fork:
git clone https://github.com/AliSafari-IT/md-file-explorer.git - Install dependencies:
pnpm install - Build the package:
pnpm build - Make your changes and test with the demo:
pnpm dev - Submit a pull request
📄 License
MIT License — see LICENSE for details.
🔗 Links
- npm: npmjs.com/package/@asafarim/md-file-explorer
- GitHub: github.com/AliSafari-IT/md-file-explorer
- Live Demo: alisafari-it.github.io/md-file-explorer
- Issues: github.com/AliSafari-IT/md-file-explorer/issues
Made with ❤️ by Ali Safari
