UNPKG

fs-syn

Version:

Lightweight purely synchronous file operation utility for Node.js, built on native fs module with zero dependencies

394 lines (314 loc) 14.7 kB
# fs-syn A lightweight **purely synchronous** file operation utility for Node.js, built on the native `fs` module with **zero dependencies**. It simplifies common file and directory operations with clear TypeScript type hints, designed specifically for scenarios where synchronous operations are preferred. ## Features - 🚫 **Purely Synchronous**: All operations run synchronously—no `async/await` or promises required. - 📦 **Zero Dependencies**: Built entirely on Node.js’s native `fs` module; no external libraries to install. - 🔧 **Essential Operations**: Covers file I/O, directory management, path matching, and hash calculation. - ✅ **TypeScript Ready**: Full type definitions for type safety, IDE autocompletion, and reduced runtime errors. - 🖥️ **Cross-Platform**: Uniformly handles path separators (e.g., `/` vs `\`) across Windows, macOS, and Linux. ## Installation ### Core Package ```bash npm install fs-syn --save ``` ### TypeScript Support (Node.js < 16) For Node.js versions older than 16, install `@types/node` to get full TypeScript type hints: ```bash npm install @types/node --save-dev ``` ## Quick Start ```typescript import fs from 'fs-syn'; try { // 1. Create a directory (recursively if needed) fs.mkdir('./example'); // 2. Write content to a file (creates parent dirs automatically) fs.write('./example/hello.txt', 'Hello World!'); // 3. Read content from the file const content = fs.read('./example/hello.txt'); console.log(content); // Output: Hello World! // 4. Clean up (delete the directory and its contents) fs.remove('./example'); } catch (err: any) { console.error('Operation failed:', err.message); } ``` ## Complete API Examples All methods throw descriptive errors on failure (e.g., missing files, permission issues). Use `try/catch` to handle errors gracefully. ### 1. File Operations #### `write(file: string, content: string | Buffer, options?: fs.WriteFileOptions)` Write content to a file (automatically creates parent directories if they don’t exist). ```typescript // Write a string to a log file fs.write('./logs/access.log', 'User login at 10:00 AM'); // Write binary data (Buffer) to a file const binaryData = Buffer.from('Raw binary content', 'utf8'); fs.write('./data/bin.dat', binaryData); // Write with custom options (e.g., append mode, specific encoding) fs.write('./config.ini', 'debug=true', { encoding: 'utf8', flag: 'a' // Append to file instead of overwriting }); ``` #### `read(file: string, options?: { encoding?: BufferEncoding | null; flag?: string })` Read content from a file (returns a `string` by default, or `Buffer` if `encoding: null`). ```typescript // Read file as a UTF-8 string (default) const text = fs.read('./notes.txt'); console.log('File content:', text); // Read file as a Buffer (for binary files like images) const imageBuffer = fs.read('./assets/logo.png', { encoding: null }); // Read with a custom flag (e.g., read-only mode) const lockedContent = fs.read('./protected.txt', { flag: 'r' }); ``` #### `readJSON<T>(file: string, options?: { encoding?: BufferEncoding | null; flag?: string })` Read and parse a JSON file directly (returns a typed object for TypeScript users). ```typescript // Define a TypeScript interface for type safety interface AppConfig { port: number; debug: boolean; theme: string; } // Read and parse JSON with type hints const config = fs.readJSON<AppConfig>('./config.json'); console.log('Server port:', config.port); // Type-checked: config.port is number // Read with a custom encoding (e.g., ASCII) const legacyData = fs.readJSON('./legacy-data.json', { encoding: 'ascii' }); ``` #### `writeJSON(file: string, data: unknown, spaces?: number)` Write a JavaScript object to a JSON file with automatic formatting. ```typescript // Write a user object to JSON (2-space indent by default) const user = { id: 123, name: 'John Doe', roles: ['editor', 'admin'] }; fs.writeJSON('./users/john.json', user); // Write with 4-space indent for readability fs.writeJSON('./config.json', { theme: 'dark', timeout: 5000 }, 4); ``` #### `appendFile(file: string, content: string | Buffer, options?: fs.WriteFileOptions)` Append content to an existing file (creates the file if it doesn’t exist). ```typescript // Append a log entry with a timestamp const logEntry = `[INFO] Server restarted at ${new Date().toISOString()}\n`; fs.appendFile('./app.log', logEntry); // Append binary data to a log file const sensorData = Buffer.from([0x01, 0x0A, 0xFF]); // Example sensor values fs.appendFile('./sensor/log.dat', sensorData); ``` #### `copy(from: string, to: string, options?: { force?: boolean; preserveTimestamps?: boolean })` Recursively copy files or directories (supports overwriting and preserving file timestamps). ```typescript // Copy a single file to a new location fs.copy('./docs/manual.pdf', './public/downloads/user-manual.pdf'); // Copy an entire directory (force overwrite if target exists) fs.copy('./src', './src-backup', { force: true }); // Copy with preserved timestamps (retains original create/modify times) fs.copy('./archive/2023', './backup/2023', { preserveTimestamps: true }); ``` #### `move(from: string, to: string, options?: { force?: boolean })` Move or rename files/directories (falls back to "copy + delete" for cross-device moves). ```typescript // Rename a file (same directory) fs.move('./tmp/report-draft.txt', './reports/final-report.txt'); // Move a directory to a new parent folder fs.move('./old-docs', './archive/2024-docs'); // Force overwrite an existing target file fs.move('./new-config.json', './current-config.json', { force: true }); ``` #### `remove(path: string)` Recursively delete files or directories (equivalent to `rm -rf` on Unix or `rmdir /s /q` on Windows). ```typescript // Delete a single file fs.remove('./temp.log'); // Delete an entire directory (and all its contents) fs.remove('./node_modules'); // Delete a nested subdirectory fs.remove('./dist/assets/old-images'); ``` #### `hashFile(file: string, algorithm?: string)` Calculate a cryptographic hash for a file (supports all algorithms Node.js’s `crypto` module offers). ```typescript // Calculate SHA-256 hash (default algorithm) const sha256Hash = fs.hashFile('./downloads/installer.exe'); console.log('SHA-256:', sha256Hash); // Calculate MD5 hash (common for file integrity checks) const md5Hash = fs.hashFile('./docs.pdf', 'md5'); console.log('MD5:', md5Hash); // Calculate SHA-1 hash (legacy use cases) const sha1Hash = fs.hashFile('./legacy-data.bin', 'sha1'); ``` ### 2. Directory Operations #### `mkdir(dirPath: string)` Create a directory (and all parent directories) recursively (equivalent to `mkdir -p` on Unix). ```typescript // Create a single directory fs.mkdir('./new-folder'); // Create nested directories (no need to create parents first) fs.mkdir('./src/assets/images/icons'); // Create a directory with spaces in the name fs.mkdir('./docs/user guides'); ``` #### `readDir(dir: string, options?: { withFileTypes?: boolean })` Read the contents of a directory (returns filenames by default, or `fs.Dirent` objects for type info). ```typescript // Read directory contents as an array of filenames const files = fs.readDir('./src'); console.log('Files in src:', files); // e.g., ["index.ts", "utils/"] // Read with file type information (distinguish files vs directories) const entries = fs.readDir('./docs', { withFileTypes: true }); entries.forEach(entry => { if (entry.isDirectory()) { console.log(`Directory: ${entry.name}`); } else { console.log(`File: ${entry.name}`); } }); ``` #### `emptyDir(dir: string)` Empty the contents of a directory **without deleting the directory itself**. ```typescript // Empty a log directory (keep the ./logs folder) fs.emptyDir('./logs'); // Empty a cache directory (retain the ./node_modules/.cache folder) fs.emptyDir('./node_modules/.cache'); ``` #### `isDir(path: string)` Check if a given path points to an existing directory. ```typescript // Check if a directory exists if (fs.isDir('./src')) { console.log('./src is a valid directory'); } // Dynamic path check const targetPath = './unknown-folder'; console.log(`${targetPath} is ${fs.isDir(targetPath) ? '' : 'not '}a directory`); ``` #### `isFile(path: string)` Check if a given path points to an existing file. ```typescript // Check if a file exists and is not a directory if (fs.isFile('./package.json')) { console.log('package.json exists (and is a file)'); } // Conditionally process a file const dataFile = './data.csv'; if (fs.isFile(dataFile)) { const content = fs.read(dataFile); // Process the file content... } ``` #### `exists(...paths: string[])` Check if a path exists (supports multiple path segments to build the full path). ```typescript // Check if a single path exists if (fs.exists('./config.json')) { console.log('Configuration file found'); } // Build and check a path from multiple segments (avoids manual joining) if (fs.exists('src', 'utils', 'helpers.ts')) { console.log('helpers.ts exists in src/utils/'); } ``` ### 3. Path Utilities #### `expand(patterns: string | string[], options?: ExpandOptions)` Match paths using glob-like patterns (supports `*` for single-level matches and `**` for multi-level matches). **`ExpandOptions` Interface**: - `cwd?: string`: Working directory to search from (default: `process.cwd()`). - `dot?: boolean`: Include hidden files/directories (those starting with `.`, default: `false`). - `onlyFiles?: boolean`: Return only files (exclude directories, default: `false`). - `onlyDirs?: boolean`: Return only directories (exclude files, default: `false`). ```typescript // Find all TypeScript files in the project (recursive) const tsFiles = fs.expand('**/*.ts'); console.log('TypeScript files:', tsFiles); // e.g., ["src/index.ts", "tests/utils.ts"] // Find all JSON files in the ./src directory (non-recursive) const srcJsonFiles = fs.expand('./src/*.json'); // Find only directories matching "docs-*" (e.g., docs-v1, docs-v2) const docDirs = fs.expand('docs-*', { onlyDirs: true }); // Include hidden files (e.g., .env, .gitignore) in the ./config directory const hiddenConfigFiles = fs.expand('*', { cwd: './config', dot: true, onlyFiles: true }); ``` #### `isPathAbsolute(path: string)` Check if a path is absolute (works cross-platform). ```typescript // POSIX systems (macOS/Linux) console.log(fs.isPathAbsolute('/usr/local/bin')); // true console.log(fs.isPathAbsolute('./relative/path')); // false // Windows systems console.log(fs.isPathAbsolute('C:\\Windows\\System32')); // true console.log(fs.isPathAbsolute('..\\relative\\path')); // false ``` #### `doesPathContain(ancestor: string, ...paths: string[])` Check if one or more "child" paths are contained within an "ancestor" directory (prevents path traversal issues). ```typescript // Check if multiple files are inside the ./src directory const allInSrc = fs.doesPathContain( './src', './src/index.ts', './src/utils/helpers.ts' ); console.log('All files in src:', allInSrc); // true // Check if logs are contained in /var/log (POSIX) const logsInVar = fs.doesPathContain('/var/log', '/var/log/app.log', '/var/log/nginx'); ``` #### `createSymlink(target: string, linkPath: string, options?: { type?: 'file' | 'dir' | 'junction' })` Create a symbolic link (symlink) to a file or directory. ```typescript // Create a symlink to a file (points to ./dist/index.js) fs.createSymlink('./dist/index.js', './current.js', { type: 'file' }); // Create a symlink to a directory (points to ./docs/latest) fs.createSymlink('./docs/latest', './docs/current', { type: 'dir' }); // Create a junction (Windows-only) for network shares if (process.platform === 'win32') { fs.createSymlink('\\\\server\\data', './network-data', { type: 'junction' }); } ``` #### `realpath(path: string)` Resolve a symbolic link to its **real, physical path** (follows nested symlinks). ```typescript // Resolve a symlink to its target const realFilePath = fs.realpath('./current.js'); console.log('Symlink points to:', realFilePath); // e.g., "./dist/index.js" // Resolve nested symlinks const resolvedPath = fs.realpath('./deep/link/to/file'); ``` ## Error Handling All methods throw `Error` objects with human-readable messages. Use `try/catch` to handle specific failure scenarios: ```typescript try { // Attempt to read a non-existent file fs.read('./nonexistent-file.txt'); } catch (err: any) { console.error('Error Name:', err.name); // "Error" console.error('Error Message:', err.message); // "File not found: ./nonexistent-file.txt" console.error('Affected Path:', err.path); // (Optional) Path that caused the error } ``` ### Common Error Scenarios | Error Message | Cause | Fix | |---------------|-------|-----| | `File not found: [path]` | The target file doesn’t exist. | Verify the path, or create the file first. | | `Directory not found: [path]` | The target directory doesn’t exist. | Use `fs.mkdir` to create it first. | | `Target already exists (set force: true to overwrite): [path]` | The destination path for `copy`/`move` already exists. | Add `force: true` to the options to overwrite. | | `Path is a directory: [path]` | You tried to use a directory as a file (e.g., `fs.read('./src')`). | Verify the path points to a file, not a directory. | ## Important Notes ### 1. Synchronous Operations Block the Event Loop Synchronous I/O blocks Node.js’s event loop until the operation completes. **Use `fs-syn` only for**: - Simple scripts, CLI tools, or build processes. - Configuration file loading (at startup, before serving requests). - Small to medium-sized file processing (avoid large files that cause long delays). **Avoid** using `fs-syn` in high-performance server applications (e.g., Express/Koa APIs) where non-blocking I/O is critical. ### 2. `remove` Is Destructive The `fs.remove` method deletes files and directories recursively (like `rm -rf`). **Always double-check paths** to prevent accidental data loss (e.g., never use `fs.remove('/')` or `fs.remove('./')`). ### 3. Pattern Matching Limitations The `expand` method supports basic glob patterns (`*` and `**`), but it’s not as powerful as dedicated libraries like `glob` (e.g., no support for negation patterns like `!exclude-me.ts`). For complex matching, consider combining `fs-syn` with a lightweight glob library. ## Compatibility - **Node.js**: 14.0.0 or higher (supports the full `fs` module sync API). - **TypeScript**: 4.5 or higher (for type definitions). - **Operating Systems**: Windows, macOS, Linux. ## License MIT License. See [LICENSE](https://github.com/your-username/fs-syn/blob/main/LICENSE) for details.