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
Markdown
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.
- 🚫 **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);
}
```
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 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' });
```
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' });
```
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);
```
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);
```
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 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 });
```
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');
```
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');
```
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');
```
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}`);
}
});
```
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');
```
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`);
```
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...
}
```
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/');
}
```
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 });
```
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
```
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');
```
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' });
}
```
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');
```
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
}
```
| 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.