CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-metro-file-map

File system crawling, watching and mapping library designed for Metro bundler ecosystem

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

plugin-system.mddocs/

Plugin System

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.

Capabilities

FileMapPlugin Interface

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 })]
});

Plugin Initialization

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;
  }
}

Built-in Plugins

Metro-file-map includes several built-in plugins.

HastePlugin

/**
 * 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;
}

MockPlugin

/**
 * 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
});

File System Delta Updates

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;
  }
}

Plugin Configuration

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();

Types

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';
}

docs

cache-management.md

error-handling.md

file-map-core.md

file-system.md

haste-system.md

index.md

plugin-system.md

tile.json