Simple in-memory vinyl file store with lazy loading, streaming operations, and pipeline transformations.
npx @tessl/cli install tessl/npm-mem-fs@4.1.0mem-fs is a TypeScript library that provides an in-memory vinyl file store with lazy loading capabilities from disk, streaming operations, and pipeline transformations. It enables developers to work with files in memory while maintaining the flexibility to load from and persist to the file system on demand, making it ideal for build tools, static site generators, and file processing pipelines.
npm install mem-fsimport { create, Store } from "mem-fs";For CommonJS:
const { create, Store } = require("mem-fs");import { create } from "mem-fs";
import File from "vinyl";
// Create a store instance
const store = create();
// Load a file from disk (lazy loaded)
const file = store.get("./package.json");
console.log(file.contents?.toString());
// Add/modify files in memory
const newFile = new File({
path: "./build/output.txt",
contents: Buffer.from("Generated content")
});
store.add(newFile);
// Stream all files through processing pipeline
await store.pipeline(
// Transform files through processing steps
transform({ objectMode: true }, function(file, encoding, callback) {
// Process each file
file.contents = Buffer.from(file.contents.toString().toUpperCase());
callback(null, file);
})
);mem-fs is built around several key components:
Factory function to create new Store instances with optional custom file loaders.
/**
* Creates a new Store instance with default vinyl file loading
* @returns New Store instance
*/
function create<StoreFile extends { path: string } = File>(): Store<StoreFile>;Core file storage class with in-memory caching and lazy loading from disk.
/**
* Main file store class with generic type support
*/
class Store<StoreFile extends { path: string } = File> extends EventEmitter {
/**
* Custom file loading function
*/
loadFile: (filepath: string) => StoreFile;
/**
* Create a new Store instance
* @param options - Configuration options
*/
constructor(options?: { loadFile?: (filepath: string) => StoreFile });
/**
* Get file from memory or load from disk if not cached
* @param filepath - File path to retrieve
* @returns File object (empty vinyl file if not found)
*/
get(filepath: string): StoreFile;
/**
* Check if file exists in memory without loading from disk
* @param filepath - File path to check
* @returns True if file exists in memory
*/
existsInMemory(filepath: string): boolean;
/**
* Add or update file in store and emit change event
* @param file - File object to add
* @returns Store instance for chaining
*/
add(file: StoreFile): this;
/**
* Iterate over all files in memory
* @param onEach - Callback function for each file
* @returns Store instance for chaining
*/
each(onEach: (file: StoreFile) => void): this;
/**
* Get array of all files currently in memory
* @returns Array of all stored files
*/
all(): StoreFile[];
/**
* Create readable stream of files with optional filtering
* @param options - Stream configuration options
* @returns Readable stream of files
*/
stream(options?: StreamOptions<StoreFile>): Readable;
/**
* Process files through transformation pipeline
* @param options - Pipeline configuration options or first transform
* @param transforms - Additional transformation streams
* @returns Promise that resolves when pipeline completes
*/
pipeline(
options?: PipelineOptions<StoreFile> | FileTransform<StoreFile>,
...transforms: FileTransform<StoreFile>[]
): Promise<void>;
}Default file loader using vinyl-file with fallback for missing files.
/**
* Default file loader using vinyl-file, creates empty vinyl file if loading fails
* @param filepath - Path to file to load
* @returns Vinyl file object
*/
function loadFile(filepath: string): File;Helper function for type checking and configuration interfaces.
/**
* Type guard to check if value is a FileTransform
* @param transform - Value to check
* @returns True if value is a FileTransform
*/
function isFileTransform<StoreFile extends { path: string } = File>(
transform: PipelineOptions<StoreFile> | FileTransform<StoreFile> | undefined
): transform is FileTransform<StoreFile>;/**
* Stream transform type for file processing pipelines
*/
type FileTransform<File> = PipelineTransform<PipelineTransform<any, File>, File>;
/**
* Options for stream() method
*/
interface StreamOptions<StoreFile extends { path: string } = File> {
/** Optional file filter predicate */
filter?: (file: StoreFile) => boolean;
}
/**
* Options for pipeline() method
*/
interface PipelineOptions<StoreFile extends { path: string } = File> {
/** Optional file filter predicate */
filter?: (file: StoreFile) => boolean;
/** Conflict resolution strategy for duplicate files */
resolveConflict?: (current: StoreFile, newFile: StoreFile) => StoreFile;
/** Whether to create new store map (default: true) */
refresh?: boolean;
/** Allow overriding duplicate files (alternative to resolveConflict) */
allowOverride?: boolean;
}The Store class extends EventEmitter and emits the following events:
/**
* Emitted when files are added or modified
* @event change
* @param filepath - Path of the changed file
*/
store.on('change', (filepath: string) => void);import { create } from "mem-fs";
import File from "vinyl";
const store = create();
// Load existing file from disk
const packageFile = store.get("./package.json");
console.log(packageFile.contents?.toString());
// Check if file is already in memory
if (!store.existsInMemory("./README.md")) {
console.log("README.md will be loaded from disk");
}
// Create and add new file
const outputFile = new File({
path: "./dist/bundle.js",
contents: Buffer.from("console.log('Hello world');")
});
store.add(outputFile);
// Listen for file changes
store.on('change', (filepath) => {
console.log(`File changed: ${filepath}`);
});// Iterate over all files
store.each((file) => {
console.log(`Processing: ${file.path}`);
// Modify file contents
if (file.path.endsWith('.js')) {
file.contents = Buffer.from(`// Generated\n${file.contents?.toString()}`);
}
});
// Get all files as array
const allFiles = store.all();
console.log(`Total files: ${allFiles.length}`);import { Transform } from "stream";
// Create filtered stream
const jsFiles = store.stream({
filter: (file) => file.path.endsWith('.js')
});
jsFiles.on('data', (file) => {
console.log(`JavaScript file: ${file.path}`);
});
// Process files through pipeline
await store.pipeline(
{ filter: (file) => file.path.endsWith('.md') },
new Transform({
objectMode: true,
transform(file, encoding, callback) {
// Convert markdown files to uppercase
file.contents = Buffer.from(file.contents?.toString().toUpperCase() || '');
callback(null, file);
}
})
);import { Duplex } from "stream";
// Complex pipeline with conflict resolution
await store.pipeline(
{
filter: (file) => file.path.includes('src/'),
resolveConflict: (current, newFile) => {
// Keep the newer file based on modification time
return newFile.stat?.mtime > current.stat?.mtime ? newFile : current;
}
},
// Transform stream that renames files
Duplex.from(async function* (files) {
for await (const file of files) {
file.path = file.path.replace('src/', 'build/');
yield file;
}
}),
// Minification transform
new Transform({
objectMode: true,
transform(file, encoding, callback) {
if (file.path.endsWith('.js')) {
// Simulate minification
const content = file.contents?.toString().replace(/\s+/g, ' ') || '';
file.contents = Buffer.from(content);
}
callback(null, file);
}
})
);// Create store with custom file loader
const customStore = new Store({
loadFile: (filepath) => {
return new File({
path: filepath,
contents: Buffer.from(`// Custom loaded: ${filepath}`)
});
}
});
const customFile = customStore.get('./any-file.js');
console.log(customFile.contents?.toString()); // "// Custom loaded: ./any-file.js"get() returns empty vinyl files with contents: null for non-existent or unreadable filespipeline() throws errors for duplicate files unless conflict resolution is configured via resolveConflict or allowOverridepath.resolve() for consistencyloadFile functions should be handled by the implementer