UNPKG

@catbee/utils

Version:

A modular, production-grade utility toolkit for Node.js and TypeScript, designed for robust, scalable applications (including Express-based services). All utilities are tree-shakable and can be imported independently.

480 lines 18.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ensureDir = ensureDir; exports.listFiles = listFiles; exports.deleteDirRecursive = deleteDirRecursive; exports.isDirectory = isDirectory; exports.copyDir = copyDir; exports.moveDir = moveDir; exports.emptyDir = emptyDir; exports.getDirSize = getDirSize; exports.watchDir = watchDir; exports.findFilesByPattern = findFilesByPattern; exports.getSubdirectories = getSubdirectories; exports.ensureEmptyDir = ensureEmptyDir; exports.createTempDir = createTempDir; exports.findNewestFile = findNewestFile; exports.findOldestFile = findOldestFile; exports.findInDir = findInDir; exports.watchDirRecursive = watchDirRecursive; exports.getDirStats = getDirStats; exports.walkDir = walkDir; const os_1 = __importDefault(require("os")); const fs_1 = __importDefault(require("fs")); const promises_1 = __importDefault(require("fs/promises")); const path_1 = __importDefault(require("path")); const glob_1 = require("glob"); const crypto_1 = require("crypto"); /** * Ensures that a directory exists, creating parent directories if needed (like `mkdir -p`). * * @param {string} dirPath - The directory path to ensure. * @returns {Promise<void>} Resolves when the directory exists. * @throws {Error} If directory cannot be created. */ async function ensureDir(dirPath) { await promises_1.default.mkdir(dirPath, { recursive: true }); } /** * Recursively lists all files in a directory. * * @param {string} dirPath - The base directory. * @param {boolean} [recursive=false] - Whether to recurse into subdirectories. * @returns {Promise<string[]>} Array of absolute file paths. * @throws {Error} If the directory cannot be read. */ async function listFiles(dirPath, recursive = false) { const entries = await promises_1.default.readdir(dirPath, { withFileTypes: true }); const files = []; for (const entry of entries) { const fullPath = path_1.default.join(dirPath, entry.name); if (entry.isFile()) { files.push(fullPath); } else if (recursive && entry.isDirectory()) { const nestedFiles = await listFiles(fullPath, true); files.push(...nestedFiles); } } return files; } /** * Deletes a directory and all its contents recursively (like `rm -rf`). * * @param {string} dirPath - Directory to delete. * @returns {Promise<void>} Resolves when deletion is complete. * @throws {Error} If deletion fails. */ async function deleteDirRecursive(dirPath) { await promises_1.default.rm(dirPath, { recursive: true, force: true }); } /** * Checks whether a given path is a directory. * * @param {string} pathStr - Path to check. * @returns {Promise<boolean>} True if the path is a directory, else false. */ async function isDirectory(pathStr) { try { const stat = await promises_1.default.stat(pathStr); return stat.isDirectory(); } catch (_a) { return false; } } /** * Recursively copies a directory and all its contents to a destination. * * @param {string} src - Source directory path. * @param {string} dest - Destination directory path. * @returns {Promise<void>} Resolves when copy is complete. * @throws {Error} If source does not exist or copy fails. */ async function copyDir(src, dest) { await ensureDir(dest); const entries = await promises_1.default.readdir(src, { withFileTypes: true }); for (const entry of entries) { const srcPath = path_1.default.join(src, entry.name); const destPath = path_1.default.join(dest, entry.name); if (entry.isDirectory()) { await copyDir(srcPath, destPath); } else { await promises_1.default.copyFile(srcPath, destPath); } } } /** * Moves a directory to a new location by copying and deleting the original. * * @param {string} src - Source directory path. * @param {string} dest - Destination directory path. * @returns {Promise<void>} Resolves when move is complete. * @throws {Error} If copy or deletion fails. */ async function moveDir(src, dest) { await copyDir(src, dest); await deleteDirRecursive(src); } /** * Empties a directory by deleting all files and subdirectories inside it. * * @param {string} dirPath - Path to the directory to empty. * @returns {Promise<void>} Resolves when the directory has been emptied. * @throws {Error} If files or subdirectories cannot be removed. */ async function emptyDir(dirPath) { const entries = await promises_1.default.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const entryPath = path_1.default.join(dirPath, entry.name); if (entry.isDirectory()) { await promises_1.default.rm(entryPath, { recursive: true, force: true }); } else { await promises_1.default.unlink(entryPath); } } } /** * Calculates the total size (in bytes) of all files in a directory (recursive). * * @param {string} dirPath - Path to the directory. * @returns {Promise<number>} Total size in bytes. * @throws {Error} If any file stats cannot be read. */ async function getDirSize(dirPath) { let total = 0; const entries = await promises_1.default.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const entryPath = path_1.default.join(dirPath, entry.name); if (entry.isDirectory()) { total += await getDirSize(entryPath); } else { const stat = await promises_1.default.stat(entryPath); total += stat.size; } } return total; } /** * Watches a directory for file changes and calls a callback on each event. * * @param {string} dirPath - Directory path to watch. * @param {(eventType: "rename" | "change", filename: string | null) => void} callback - Callback for each change event. * @returns {() => void} A function to stop watching the directory. */ function watchDir(dirPath, callback) { const watcher = fs_1.default.watch(dirPath, (eventType, filename) => { callback(eventType, filename); }); return () => watcher.close(); } /** * Finds files matching a glob pattern. * * @param {string} pattern - Glob pattern to match files. * @param {object} [options] - Options for glob pattern matching. * @param {string} [options.cwd] - Current working directory for relative patterns. * @param {boolean} [options.dot=false] - Include dotfiles in matches. * @param {boolean} [options.nodir=true] - Only match files, not directories. * @returns {Promise<string[]>} Array of matched file paths. * @throws {Error} If pattern matching fails. */ async function findFilesByPattern(pattern, options = {}) { const defaultOptions = Object.assign({ nodir: true }, options); return (0, glob_1.glob)(pattern, defaultOptions); } /** * Gets all subdirectories in a directory. * * @param {string} dirPath - The directory to search in. * @param {boolean} [recursive=false] - Whether to include subdirectories recursively. * @returns {Promise<string[]>} Array of absolute subdirectory paths. * @throws {Error} If directory cannot be read. */ async function getSubdirectories(dirPath, recursive = false) { const entries = await promises_1.default.readdir(dirPath, { withFileTypes: true }); const dirs = entries .filter((entry) => entry.isDirectory()) .map((entry) => path_1.default.join(dirPath, entry.name)); if (recursive && dirs.length > 0) { const nestedDirs = await Promise.all(dirs.map((dir) => getSubdirectories(dir, true))); return [...dirs, ...nestedDirs.flat()]; } return dirs; } /** * Ensures a directory exists and is empty. * * @param {string} dirPath - Path to the directory. * @returns {Promise<void>} Resolves when the directory exists and is empty. * @throws {Error} If directory cannot be created or emptied. */ async function ensureEmptyDir(dirPath) { if (await isDirectory(dirPath)) { await emptyDir(dirPath); } else { try { await promises_1.default.unlink(dirPath); } catch (error) { // Ignore if file doesn't exist if (error.code !== "ENOENT") { throw error; } } await ensureDir(dirPath); } } /** * Creates a temporary directory with optional auto-cleanup. * * @param {object} [options] - Options for the temporary directory. * @param {string} [options.prefix='tmp-'] - Prefix for the directory name. * @param {string} [options.parentDir=os.tmpdir()] - Parent directory. * @param {boolean} [options.cleanup=false] - Whether to register cleanup on process exit. * @returns {Promise<{ path: string, cleanup: () => Promise<void> }>} Object with directory path and cleanup function. * @throws {Error} If directory cannot be created. */ async function createTempDir(options = {}) { const prefix = options.prefix || "tmp-"; const parentDir = options.parentDir || os_1.default.tmpdir(); const dirName = `${prefix}${(0, crypto_1.randomBytes)(6).toString("hex")}`; const tempDirPath = path_1.default.join(parentDir, dirName); await ensureDir(tempDirPath); const cleanup = async () => { try { await deleteDirRecursive(tempDirPath); } catch (_a) { // Ignore cleanup errors } }; if (options.cleanup) { process.once("exit", () => { try { fs_1.default.rmSync(tempDirPath, { recursive: true, force: true }); } catch (_a) { // Ignore cleanup errors on exit } }); // Handle signals for better cleanup ["SIGINT", "SIGTERM", "SIGUSR1", "SIGUSR2"].forEach((signal) => { process.once(signal, () => { cleanup().then(() => process.exit()); }); }); } return { path: tempDirPath, cleanup }; } /** * Finds the newest file in a directory. * * @param {string} dirPath - Directory to search. * @param {boolean} [recursive=false] - Whether to search subdirectories. * @returns {Promise<string | null>} Path to the newest file or null if no files. * @throws {Error} If directory cannot be read. */ async function findNewestFile(dirPath, recursive = false) { const files = await listFiles(dirPath, recursive); if (files.length === 0) return null; const stats = await Promise.all(files.map(async (file) => ({ path: file, mtime: (await promises_1.default.stat(file)).mtime, }))); return stats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())[0].path; } /** * Finds the oldest file in a directory. * * @param {string} dirPath - Directory to search. * @param {boolean} [recursive=false] - Whether to search subdirectories. * @returns {Promise<string | null>} Path to the oldest file or null if no files. * @throws {Error} If directory cannot be read. */ async function findOldestFile(dirPath, recursive = false) { const files = await listFiles(dirPath, recursive); if (files.length === 0) return null; const stats = await Promise.all(files.map(async (file) => ({ path: file, mtime: (await promises_1.default.stat(file)).mtime, }))); return stats.sort((a, b) => a.mtime.getTime() - b.mtime.getTime())[0].path; } /** * Finds files or directories in a directory matching a predicate function. * * @param {string} dirPath - Directory to search. * @param {(path: string, stat: fs.Stats) => boolean | Promise<boolean>} predicate - Function to test each path. * @param {boolean} [recursive=false] - Whether to search subdirectories. * @returns {Promise<string[]>} Array of matching paths. * @throws {Error} If directory cannot be read. */ async function findInDir(dirPath, predicate, recursive = false) { const entries = await promises_1.default.readdir(dirPath, { withFileTypes: true }); const results = []; for (const entry of entries) { const entryPath = path_1.default.join(dirPath, entry.name); const stat = await promises_1.default.stat(entryPath); if (await predicate(entryPath, stat)) { results.push(entryPath); } if (recursive && entry.isDirectory()) { const nestedResults = await findInDir(entryPath, predicate, true); results.push(...nestedResults); } } return results; } /** * Watches a directory recursively for file changes. * * @param {string} dirPath - Base directory path to watch. * @param {(eventType: "rename" | "change", filename: string) => void} callback - Callback for each change event. * @param {boolean} [includeSubdirs=true] - Whether to watch subdirectories. * @returns {Promise<() => void>} A function to stop watching the directory. * @throws {Error} If directory cannot be watched. */ async function watchDirRecursive(dirPath, callback, includeSubdirs = true) { // Normalize the path to ensure consistent path separators const normalizedBasePath = path_1.default.normalize(dirPath); // Create watchers for the base dir and all subdirectories const watchers = []; // Helper function to add a watcher for a directory const addWatcher = (dir) => { try { const watcher = fs_1.default.watch(dir, (eventType, filename) => { if (!filename) return; const fullPath = path_1.default.join(dir, filename); // Make the path relative to the base directory const relativePath = path_1.default.relative(normalizedBasePath, fullPath); callback(eventType, relativePath); // If a new directory is created, we should watch it if (eventType === "rename" && includeSubdirs) { setTimeout(async () => { try { if (await isDirectory(fullPath)) { addWatcher(fullPath); } } catch (_a) { // Ignore errors checking newly created items } }, 100); } }); watchers.push(watcher); } catch (_a) { // Silently ignore directories we can't watch } }; // Add watcher for the base directory addWatcher(normalizedBasePath); // If watching subdirectories, add watchers for all existing subdirectories if (includeSubdirs) { const subdirs = await getSubdirectories(normalizedBasePath, true); for (const subdir of subdirs) { addWatcher(subdir); } } // Return a function to close all watchers return () => { watchers.forEach((watcher) => watcher.close()); }; } /** * Gets detailed directory statistics including file count, directory count, and size. * * @param {string} dirPath - Path to the directory. * @returns {Promise<{ fileCount: number, dirCount: number, totalSize: number }>} Directory statistics. * @throws {Error} If directory cannot be read. */ async function getDirStats(dirPath) { let fileCount = 0; let dirCount = 0; let totalSize = 0; async function processDir(currentPath) { const entries = await promises_1.default.readdir(currentPath, { withFileTypes: true }); for (const entry of entries) { const entryPath = path_1.default.join(currentPath, entry.name); if (entry.isDirectory()) { dirCount++; await processDir(entryPath); } else if (entry.isFile()) { fileCount++; const stat = await promises_1.default.stat(entryPath); totalSize += stat.size; } } } await processDir(dirPath); return { fileCount, dirCount, totalSize }; } /** * Walks through a directory hierarchy, calling a visitor function for each entry. * * @param {string} dirPath - Starting directory path. * @param {object} options - Options for walking the directory. * @param {(entry: { path: string, name: string, isDirectory: boolean, stats: fs.Stats }) => boolean | void | Promise<boolean | void>} options.visitorFn - * Function called for each file/directory. Return false to skip a directory. * @param {'pre' | 'post'} [options.traversalOrder='pre'] - Whether to visit directories before or after their contents. * @throws {Error} If directory cannot be read. */ async function walkDir(dirPath, options) { const traversalOrder = options.traversalOrder || "pre"; const entries = await promises_1.default.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const entryPath = path_1.default.join(dirPath, entry.name); const stats = await promises_1.default.stat(entryPath); const entryInfo = { path: entryPath, name: entry.name, isDirectory: entry.isDirectory(), stats, }; if (entry.isDirectory()) { // Pre-order: visit directory before its contents if (traversalOrder === "pre") { const shouldContinue = await options.visitorFn(entryInfo); // If the visitor returns false, don't recurse into this directory if (shouldContinue !== false) { await walkDir(entryPath, options); } } else { // Post-order: visit directory after its contents await walkDir(entryPath, options); await options.visitorFn(entryInfo); } } else { // Files are always visited when encountered await options.visitorFn(entryInfo); } } if (traversalOrder === "post") { // Only visit the root if this is the top-level call (not for subdirectories) // But since this function is always called recursively, always visit after children const stats = await promises_1.default.stat(dirPath); const entryInfo = { path: dirPath, name: path_1.default.basename(dirPath), isDirectory: true, stats, }; await options.visitorFn(entryInfo); } } //# sourceMappingURL=dir.utils.js.map