@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
JavaScript
"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