File system crawling, watching and mapping library designed for Metro bundler ecosystem
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Extensible plugin system for Haste modules, mocks, and custom file processing. Plugins enable extending metro-file-map functionality with custom file analysis, module resolution strategies, and data processing workflows.
Core interface that all plugins must implement.
/**
* Plugin interface for extending FileMap functionality
*/
interface FileMapPlugin<SerializableState = unknown> {
/** Unique plugin name for identification */
+name: string;
/**
* Initialize plugin with file system state and cached plugin data
* @param initOptions - Initialization options with files and plugin state
* @returns Promise that resolves when initialization completes
*/
initialize(
initOptions: FileMapPluginInitOptions<SerializableState>
): Promise<void>;
/**
* Validate plugin state and throw if invalid
* Used to detect conflicts or inconsistencies
*/
assertValid(): void;
/**
* Apply bulk file system changes
* @param delta - File system changes to process
* @returns Promise that resolves when update completes
*/
bulkUpdate(delta: FileMapDelta): Promise<void>;
/**
* Get serializable plugin state for caching
* @returns Serializable state data
*/
getSerializableSnapshot(): SerializableState;
/**
* Handle individual file removal
* @param relativeFilePath - Path of removed file
* @param fileMetadata - Metadata of removed file
*/
onRemovedFile(relativeFilePath: string, fileMetadata: FileMetadata): void;
/**
* Handle individual file addition or modification
* @param relativeFilePath - Path of added/modified file
* @param fileMetadata - Metadata of added/modified file
*/
onNewOrModifiedFile(relativeFilePath: string, fileMetadata: FileMetadata): void;
/**
* Get cache key for plugin configuration
* @returns Cache key string for invalidation
*/
getCacheKey(): string;
}Usage Examples:
// Implement custom plugin
class CustomAnalysisPlugin {
constructor(options) {
this.name = 'CustomAnalysisPlugin';
this.options = options;
this.analysisData = new Map();
}
async initialize({ files, pluginState }) {
if (pluginState) {
// Restore from cached state
this.analysisData = new Map(pluginState.analysisData);
} else {
// Initialize from file system
for (const { canonicalPath, metadata } of files.metadataIterator({
includeNodeModules: false,
includeSymlinks: false
})) {
await this.analyzeFile(canonicalPath, metadata);
}
}
}
assertValid() {
// Check for any validation errors
if (this.analysisData.size === 0) {
throw new Error('No files analyzed');
}
}
async bulkUpdate(delta) {
// Process removed files
for (const [path, metadata] of delta.removed) {
this.analysisData.delete(path);
}
// Process added/modified files
for (const [path, metadata] of delta.addedOrModified) {
await this.analyzeFile(path, metadata);
}
}
getSerializableSnapshot() {
return {
analysisData: Array.from(this.analysisData.entries())
};
}
onRemovedFile(path, metadata) {
this.analysisData.delete(path);
}
onNewOrModifiedFile(path, metadata) {
this.analyzeFile(path, metadata);
}
getCacheKey() {
return JSON.stringify(this.options);
}
async analyzeFile(path, metadata) {
// Custom analysis logic
this.analysisData.set(path, {
analyzed: true,
timestamp: Date.now()
});
}
}
// Use custom plugin
const fileMap = new FileMap({
// ... other options
plugins: [new CustomAnalysisPlugin({ enableDeepAnalysis: true })]
});Plugins are initialized with file system state and cached data.
/**
* Options passed to plugin initialization
*/
interface FileMapPluginInitOptions<SerializableState> {
/** File system state for initial processing */
files: FileSystemState;
/** Previously cached plugin state (null if no cache) */
pluginState: SerializableState | null;
}
/**
* File system state interface for plugin initialization
*/
interface FileSystemState {
/**
* Iterate over file metadata
* @param opts - Iteration options
* @returns Iterable of file information
*/
metadataIterator(opts: {
includeNodeModules: boolean;
includeSymlinks: boolean;
}): Iterable<{
baseName: string;
canonicalPath: string;
metadata: FileMetadata;
}>;
}Usage Examples:
class FileCountPlugin {
async initialize({ files, pluginState }) {
this.counts = pluginState || { js: 0, ts: 0, json: 0, other: 0 };
if (!pluginState) {
// Count files by extension
for (const { canonicalPath } of files.metadataIterator({
includeNodeModules: false,
includeSymlinks: false
})) {
this.incrementCount(canonicalPath);
}
}
console.log('File counts:', this.counts);
}
incrementCount(path) {
if (path.endsWith('.js')) this.counts.js++;
else if (path.endsWith('.ts')) this.counts.ts++;
else if (path.endsWith('.json')) this.counts.json++;
else this.counts.other++;
}
getSerializableSnapshot() {
return this.counts;
}
}Metro-file-map includes several built-in plugins.
/**
* Built-in Haste module resolution plugin
*/
class HastePlugin implements FileMapPlugin {
constructor(options: HastePluginOptions);
// FileMapPlugin interface
name: 'HastePlugin';
initialize(initOptions: FileMapPluginInitOptions): Promise<void>;
assertValid(): void;
bulkUpdate(delta: FileMapDelta): Promise<void>;
getSerializableSnapshot(): HasteMapData;
onRemovedFile(relativeFilePath: string, fileMetadata: FileMetadata): void;
onNewOrModifiedFile(relativeFilePath: string, fileMetadata: FileMetadata): void;
getCacheKey(): string;
// HasteMap interface
getModule(name: string, platform?: string, supportsNativePlatform?: boolean, type?: number): string | null;
getPackage(name: string, platform?: string, supportsNativePlatform?: boolean): string | null;
computeConflicts(): Array<HasteConflict>;
}
interface HastePluginOptions {
console?: Console;
enableHastePackages: boolean;
perfLogger?: PerfLogger;
platforms: Set<string>;
rootDir: string;
failValidationOnConflicts: boolean;
}/**
* Built-in mock module plugin
*/
class MockPlugin implements FileMapPlugin {
constructor(options: MockPluginOptions);
// FileMapPlugin interface
name: 'MockPlugin';
initialize(initOptions: FileMapPluginInitOptions): Promise<void>;
assertValid(): void;
bulkUpdate(delta: FileMapDelta): Promise<void>;
getSerializableSnapshot(): RawMockMap;
onRemovedFile(relativeFilePath: string, fileMetadata: FileMetadata): void;
onNewOrModifiedFile(relativeFilePath: string, fileMetadata: FileMetadata): void;
getCacheKey(): string;
// MockMap interface
getMockModule(name: string): string | null;
}
interface MockPluginOptions {
console?: Console;
mocksPattern: RegExp;
rootDir: string;
throwOnModuleCollision: boolean;
}Usage Examples:
import { HastePlugin, MockPlugin } from "metro-file-map";
// Configure built-in plugins
const hastePlugin = new HastePlugin({
enableHastePackages: true,
platforms: new Set(['ios', 'android', 'web']),
rootDir: process.cwd(),
failValidationOnConflicts: true
});
const mockPlugin = new MockPlugin({
mocksPattern: /\/__mocks__\//,
rootDir: process.cwd(),
throwOnModuleCollision: false,
console: console
});
// Use with FileMap
const fileMap = new FileMap({
// ... other options
plugins: [hastePlugin],
mocksPattern: '__mocks__' // This automatically creates MockPlugin
});Plugins receive file system changes through delta updates.
/**
* File system changes passed to plugins
*/
interface FileMapDelta {
/** Files that were removed */
removed: Iterable<[string, FileMetadata]>;
/** Files that were added or modified */
addedOrModified: Iterable<[string, FileMetadata]>;
}Usage Examples:
class ChangeTrackingPlugin {
constructor() {
this.name = 'ChangeTrackingPlugin';
this.changeHistory = [];
}
async bulkUpdate(delta) {
const changes = {
timestamp: Date.now(),
removed: [],
modified: []
};
// Track removed files
for (const [path, metadata] of delta.removed) {
changes.removed.push(path);
this.onRemovedFile(path, metadata);
}
// Track added/modified files
for (const [path, metadata] of delta.addedOrModified) {
changes.modified.push(path);
this.onNewOrModifiedFile(path, metadata);
}
this.changeHistory.push(changes);
// Keep only last 100 changes
if (this.changeHistory.length > 100) {
this.changeHistory.shift();
}
}
onRemovedFile(path, metadata) {
console.log(`File removed: ${path}`);
}
onNewOrModifiedFile(path, metadata) {
console.log(`File changed: ${path}`);
}
getChangeHistory() {
return this.changeHistory;
}
}Plugins can be configured through FileMap options.
/**
* Plugin configuration in InputOptions
*/
interface InputOptions {
/** Array of custom plugins to use */
plugins?: ReadonlyArray<FileMapPlugin>;
/** Mock pattern (creates MockPlugin automatically) */
mocksPattern?: string;
/** Enable Haste packages in default HastePlugin */
enableHastePackages?: boolean;
/** Throw on module collisions */
throwOnModuleCollision?: boolean;
}Usage Examples:
// Multiple plugin configuration
const fileMap = new FileMap({
extensions: ['.js', '.ts'],
platforms: ['ios', 'android'],
retainAllFiles: false,
rootDir: process.cwd(),
roots: ['./src'],
maxWorkers: 4,
healthCheck: { enabled: false, interval: 30000, timeout: 5000, filePrefix: 'test' },
// Plugin configuration
plugins: [
new CustomAnalysisPlugin(),
new ChangeTrackingPlugin(),
new HastePlugin({
enableHastePackages: true,
platforms: new Set(['ios', 'android']),
rootDir: process.cwd(),
failValidationOnConflicts: false
})
],
// Mock configuration (creates MockPlugin)
mocksPattern: '__mocks__',
throwOnModuleCollision: false
});
// Access plugins from build result
const { hasteMap, mockMap } = await fileMap.build();type FileMetadata = [
string, // id
number, // mtime
number, // size
0 | 1, // visited
string, // dependencies
string, // sha1
0 | 1 | string // symlink
];
interface RawMockMap {
duplicates: Map<string, Set<string>>;
mocks: Map<string, string>;
version: number;
}
interface HasteMapData {
modules: Map<string, HasteMapItem>;
}
interface HasteMapItem {
[platform: string]: HasteMapItemMetadata;
}
type HasteMapItemMetadata = [string, number]; // [path, type]
interface HasteConflict {
id: string;
platform: string | null;
absolutePaths: Array<string>;
type: 'duplicate' | 'shadowing';
}