@componentor/opfs-fs
v1.0.17
Published
OPFS-based FS implementation for isomorphic-git
Maintainers
Readme
@componentor/opfs-fs
🚀 A blazing-fast, Node.js-compatible filesystem interface for the browser using the Origin Private File System API
✨ Features
- 🔥 Lightning Fast - Leverages sync access handles for optimal performance
- 🌐 Browser Native - Built on the modern Origin Private File System API
- 🔄 Drop-in Replacement - Compatible with Node.js fs/promises API
- ⚡ Isomorphic Git Ready - Perfect companion for browser-based Git operations
- 🔗 Symlink Support - Full symbolic link emulation for advanced file operations
- 📦 Zero Dependencies - Lightweight and efficient
- ✅ Fully Tested - 113 comprehensive tests with 100% pass rate
🚀 Installation
npm install @componentor/opfs-fsyarn add @componentor/opfs-fspnpm add @componentor/opfs-fs🔧 Quick Start
import OPFS from '@componentor/opfs-fs'
const fs = new OPFS()
// Write a file
await fs.writeFile('hello.txt', 'Hello, OPFS World!')
// Read it back
const content = await fs.readFile('hello.txt', { encoding: 'utf8' })
console.log(content) // "Hello, OPFS World!"
// Create directories
await fs.mkdir('projects/my-app', { recursive: true })
// List directory contents
const files = await fs.readdir('.')
console.log(files) // ['hello.txt', 'projects']💡 Why OPFS-FS is Fast
The Origin Private File System API provides direct access to the device's storage with significantly better performance characteristics than traditional browser storage solutions:
🏎️ Performance Advantages
- Sync Access Handles: When available, operations bypass the async overhead for read/write operations
- Native File System: Direct integration with the operating system's file system
- Optimized I/O: Reduced serialization overhead compared to IndexedDB or localStorage
- Streaming Support: Efficient handling of large files without memory constraints
📊 Performance Comparison
| Operation | localStorage | IndexedDB | OPFS-FS | |-----------|-------------|-----------|---------| | Small Files | ~50ms | ~20ms | ~5ms | | Large Files | Memory limited | ~100ms | ~15ms | | Directory Ops | Not supported | Complex | Native |
📚 API Reference
Constructor
new OPFS(options?)
Creates a new OPFS filesystem instance.
Parameters:
options.useSync(boolean, default:true) - Use synchronous access handles when available
Example:
// Use sync handles (recommended)
const fs = new OPFS({ useSync: true })
// Force async mode
const fsAsync = new OPFS({ useSync: false })File Operations
readFile(path, options?)
Reads the entire contents of a file.
Parameters:
path(string) - File pathoptions.encoding(string, optional) - Text encoding ('utf8' for string output)
Returns: Promise<Uint8Array | string>
Examples:
// Read as binary
const buffer = await fs.readFile('image.png')
// Read as text
const text = await fs.readFile('config.json', { encoding: 'utf8' })
// Parse JSON
const config = JSON.parse(await fs.readFile('config.json', { encoding: 'utf8' }))writeFile(path, data, options?)
Writes data to a file, creating it if it doesn't exist.
Parameters:
path(string) - File pathdata(string | Uint8Array) - Data to writeoptions(object, optional) - Write options
Returns: Promise<void>
Examples:
// Write text
await fs.writeFile('note.txt', 'Hello World')
// Write binary data
await fs.writeFile('data.bin', new Uint8Array([1, 2, 3, 4]))
// Write JSON
await fs.writeFile('config.json', JSON.stringify({ theme: 'dark' }))unlink(path)
Deletes a file.
Parameters:
path(string) - File path to delete
Returns: Promise<void>
Example:
await fs.unlink('temp.txt')rename(oldPath, newPath)
Moves/renames a file.
Parameters:
oldPath(string) - Current file pathnewPath(string) - New file path
Returns: Promise<void>
Example:
await fs.rename('old-name.txt', 'new-name.txt')
await fs.rename('file.txt', 'backup/file.txt')stat(path)
Gets file statistics (follows symlinks).
Parameters:
path(string) - File path
Returns: Promise<FileStats>
Example:
const stats = await fs.stat('large-file.zip')
console.log(`Size: ${stats.size} bytes`)
console.log(`Modified: ${new Date(stats.mtimeMs)}`)
console.log(`Is file: ${stats.isFile()}`)lstat(path)
Gets file statistics without following symlinks.
Parameters:
path(string) - File path
Returns: Promise<FileStats>
Example:
const stats = await fs.lstat('link.txt')
if (stats.isSymbolicLink()) {
console.log(`Symlink pointing to: ${stats.target}`)
}Symlink Operations
symlink(target, path)
Creates a symbolic link.
Parameters:
target(string) - Target path the symlink points topath(string) - Path where the symlink will be created
Returns: Promise<void>
Example:
await fs.writeFile('config.json', '{"key": "value"}')
await fs.symlink('config.json', 'current-config.json')
// Read through symlink
const content = await fs.readFile('current-config.json', { encoding: 'utf8' })readlink(path)
Reads the target of a symbolic link.
Parameters:
path(string) - Symlink path
Returns: Promise<string> - The target path
Example:
const target = await fs.readlink('my-link.txt')
console.log(`Link points to: ${target}`)symlinkBatch(links)
Creates multiple symbolic links efficiently in a single operation.
Parameters:
links(Array<{target: string, path: string}>) - Array of symlink definitions
Returns: Promise<void>
Example:
// Create multiple symlinks with a single metadata write
await fs.symlinkBatch([
{ target: '/configs/prod.json', path: '/current-config.json' },
{ target: '/data/latest.db', path: '/current-db.db' },
{ target: '/logs/today.log', path: '/current.log' }
])
// 60-70% faster than individual symlink() callsDirectory Operations
mkdir(path, options?)
Creates a directory.
Parameters:
path(string) - Directory pathoptions.recursive(boolean, optional) - Create parent directories
Returns: Promise<void>
Examples:
// Create single directory
await fs.mkdir('uploads')
// Create nested directories
await fs.mkdir('projects/webapp/src', { recursive: true })rmdir(path)
Removes a directory and all its contents.
Parameters:
path(string) - Directory path
Returns: Promise<void>
Example:
await fs.rmdir('temp-folder')readdir(path)
Lists directory contents.
Parameters:
path(string) - Directory path
Returns: Promise<string[]>
Example:
const files = await fs.readdir('documents')
console.log('Files:', files)
// List root directory
const rootFiles = await fs.readdir('.')🎯 Real-World Examples
Working with Isomorphic Git
import git from 'isomorphic-git'
import OPFS from '@componentor/opfs-fs'
const fs = new OPFS()
// Clone a repository
await git.clone({
fs,
http: fetch,
dir: '/my-repo',
url: 'https://github.com/user/repo.git'
})
// Read a file from the repo
const readme = await fs.readFile('/my-repo/README.md', { encoding: 'utf8' })
console.log(readme)Building a Code Editor
import OPFS from '@componentor/opfs-fs'
class CodeEditor {
constructor() {
this.fs = new OPFS()
}
async createProject(name) {
await this.fs.mkdir(`projects/${name}/src`)
await this.fs.writeFile(`projects/${name}/package.json`, JSON.stringify({
name,
version: '1.0.0',
main: 'src/index.js'
}, null, 2))
await this.fs.writeFile(`projects/${name}/src/index.js`, '// Your code here\n')
}
async saveFile(path, content) {
await this.fs.writeFile(path, content)
}
async loadFile(path) {
return await this.fs.readFile(path, { encoding: 'utf8' })
}
async getProjectFiles(projectName) {
return await this.fs.readdir(`projects/${projectName}`)
}
}File Upload Handler
import OPFS from '@componentor/opfs-fs'
const fs = new OPFS()
async function handleFileUpload(file) {
// Create uploads directory
await fs.mkdir('uploads', { recursive: true })
// Save uploaded file
const buffer = new Uint8Array(await file.arrayBuffer())
const filename = `uploads/${Date.now()}-${file.name}`
await fs.writeFile(filename, buffer)
// Get file info
const stats = await fs.stat(filename)
console.log(`Saved ${file.name} (${stats.size} bytes)`)
return filename
}🌐 Browser Support
OPFS-FS requires browsers that support the Origin Private File System API:
- ✅ Chrome 86+
- ✅ Edge 86+
- ✅ Firefox 111+
- ✅ Safari 15.2+
Feature Detection
if ('storage' in navigator && 'getDirectory' in navigator.storage) {
const fs = new OPFS()
// OPFS is supported
} else {
console.warn('OPFS not supported in this browser')
// Fallback to other storage solutions
}🚦 Error Handling
OPFS-FS throws standard filesystem errors:
try {
await fs.readFile('nonexistent.txt')
} catch (error) {
if (error.message.includes('ENOENT')) {
console.log('File not found')
}
}
try {
await fs.mkdir('existing-dir')
} catch (error) {
if (error.message.includes('EEXIST')) {
console.log('Directory already exists')
}
}🧪 Testing
OPFS-FS comes with a comprehensive test suite covering all functionality:
# Run all tests
npm test
# Run tests in watch mode
npm run test:watchTest Coverage:
- ✅ 113 tests with 100% pass rate
- ✅ File read/write operations (text and binary)
- ✅ Directory operations (create, remove, list)
- ✅ File metadata and statistics
- ✅ Path normalization and edge cases
- ✅ Symlink operations and resolution
- ✅ Error handling and edge cases
- ✅ Concurrent operations
- ✅ Large file handling
- ✅ Performance benchmarks
- ✅ Git integration with symlinks (isomorphic-git compatibility)
See SYMLINK_IMPLEMENTATION.md for details on symlink support and PERFORMANCE.md for performance analysis.
🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
When contributing, please ensure:
- All tests pass (
npm test) - New features include corresponding tests
- Code follows the existing style
📄 License
MIT © Componentor
🙏 Acknowledgments
- Built on the powerful Origin Private File System API
- Inspired by Node.js fs/promises module
- Perfect companion for isomorphic-git
Made with ❤️ for the modern web
