@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.
391 lines (387 loc) • 11.9 kB
JavaScript
/*
* The MIT License
*
* Copyright (c) 2026 Catbee Technologies. https://catbee.in/license
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import os from 'os';
import fs from 'fs';
import fsp from 'fs/promises';
import path from 'path';
import { randomBytes } from 'crypto';
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
async function ensureDir(dirPath) {
await fsp.mkdir(dirPath, {
recursive: true
});
}
__name(ensureDir, "ensureDir");
async function listFiles(dirPath, recursive = false) {
const entries = await fsp.readdir(dirPath, {
withFileTypes: true
});
const files = [];
for (const entry of entries) {
const fullPath = path.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;
}
__name(listFiles, "listFiles");
async function deleteDirRecursive(dirPath) {
await fsp.rm(dirPath, {
recursive: true,
force: true
});
}
__name(deleteDirRecursive, "deleteDirRecursive");
async function isDirectory(pathStr) {
try {
const stat = await fsp.stat(pathStr);
return stat.isDirectory();
} catch {
return false;
}
}
__name(isDirectory, "isDirectory");
async function copyDir(src, dest) {
await ensureDir(dest);
const entries = await fsp.readdir(src, {
withFileTypes: true
});
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
await copyDir(srcPath, destPath);
} else {
await fsp.copyFile(srcPath, destPath);
}
}
}
__name(copyDir, "copyDir");
async function moveDir(src, dest) {
await copyDir(src, dest);
await deleteDirRecursive(src);
}
__name(moveDir, "moveDir");
async function emptyDir(dirPath) {
const entries = await fsp.readdir(dirPath, {
withFileTypes: true
});
for (const entry of entries) {
const entryPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
await fsp.rm(entryPath, {
recursive: true,
force: true
});
} else {
await fsp.unlink(entryPath);
}
}
}
__name(emptyDir, "emptyDir");
async function getDirSize(dirPath) {
let total = 0;
const entries = await fsp.readdir(dirPath, {
withFileTypes: true
});
for (const entry of entries) {
const entryPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
total += await getDirSize(entryPath);
} else {
const stat = await fsp.stat(entryPath);
total += stat.size;
}
}
return total;
}
__name(getDirSize, "getDirSize");
function watchDir(dirPath, callback) {
const watcher = fs.watch(dirPath, (eventType, filename) => {
callback(eventType, filename);
});
return () => watcher.close();
}
__name(watchDir, "watchDir");
async function findFilesByPattern(pattern, options = {}) {
const matchPattern = /* @__PURE__ */ __name((name, pattern2) => {
let escaped = pattern2.replaceAll(/[.+^${}()|[\]\\]/g, String.raw`\$&`);
escaped = escaped.replaceAll("*", ".*").replaceAll("?", ".");
const regex = new RegExp(`^${escaped}$`);
return regex.test(name);
}, "matchPattern");
const { cwd = process.cwd(), dot = false } = options;
const segments = pattern.split("/");
async function walk(dir, segIndex) {
if (segIndex >= segments.length) return [];
const segment = segments[segIndex];
const entries = await fsp.readdir(dir, {
withFileTypes: true
});
let matchedFiles = [];
for (const entry of entries) {
const entryName = entry.name;
if (!dot && entryName.startsWith(".")) continue;
const fullPath = path.join(dir, entryName);
const isLastSegment = segIndex === segments.length - 1;
if (isLastSegment) {
if (matchPattern(entryName, segment) && entry.isFile()) {
matchedFiles.push(fullPath);
}
} else if (entry.isDirectory()) {
if (matchPattern(entryName, segment)) {
matchedFiles.push(...await walk(fullPath, segIndex + 1));
}
}
}
return matchedFiles;
}
__name(walk, "walk");
return walk(cwd, 0);
}
__name(findFilesByPattern, "findFilesByPattern");
async function getSubdirectories(dirPath, recursive = false) {
const entries = await fsp.readdir(dirPath, {
withFileTypes: true
});
const dirs = entries.filter((entry) => entry.isDirectory()).map((entry) => path.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;
}
__name(getSubdirectories, "getSubdirectories");
async function ensureEmptyDir(dirPath) {
if (await isDirectory(dirPath)) {
await emptyDir(dirPath);
} else {
try {
await fsp.unlink(dirPath);
} catch (error) {
if (error.code !== "ENOENT") {
throw error;
}
}
await ensureDir(dirPath);
}
}
__name(ensureEmptyDir, "ensureEmptyDir");
async function createTempDir(options = {}) {
const prefix = options.prefix || "tmp-";
const parentDir = options.parentDir || os.tmpdir();
const dirName = `${prefix}${randomBytes(6).toString("hex")}`;
const tempDirPath = path.join(parentDir, dirName);
await ensureDir(tempDirPath);
const cleanup = /* @__PURE__ */ __name(async () => {
try {
await deleteDirRecursive(tempDirPath);
} catch {
}
}, "cleanup");
if (options.cleanup) {
process.once("exit", () => {
try {
fs.rmSync(tempDirPath, {
recursive: true,
force: true
});
} catch {
}
});
const signals = [
"SIGINT",
"SIGTERM",
"SIGUSR1",
"SIGUSR2"
];
for (const signal of signals) {
process.once(signal, () => {
cleanup().then(() => process.exit());
});
}
}
return {
path: tempDirPath,
cleanup
};
}
__name(createTempDir, "createTempDir");
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 fsp.stat(file)).mtime
})));
const sorted = stats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
return sorted[0].path;
}
__name(findNewestFile, "findNewestFile");
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 fsp.stat(file)).mtime
})));
return stats.sort((a, b) => a.mtime.getTime() - b.mtime.getTime())[0].path;
}
__name(findOldestFile, "findOldestFile");
async function findInDir(dirPath, predicate, recursive = false) {
const entries = await fsp.readdir(dirPath, {
withFileTypes: true
});
const results = [];
for (const entry of entries) {
const entryPath = path.join(dirPath, entry.name);
const stat = await fsp.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;
}
__name(findInDir, "findInDir");
async function watchDirRecursive(dirPath, callback, includeSubdirs = true) {
const normalizedBasePath = path.normalize(dirPath);
const watchers = [];
const addWatcher = /* @__PURE__ */ __name((dir) => {
try {
const watcher = fs.watch(dir, (eventType, filename) => {
if (!filename) return;
const fullPath = path.join(dir, filename);
const relativePath = path.relative(normalizedBasePath, fullPath);
callback(eventType, relativePath);
if (eventType === "rename" && includeSubdirs) {
setTimeout(async () => {
try {
if (await isDirectory(fullPath)) {
addWatcher(fullPath);
}
} catch {
}
}, 100);
}
});
watchers.push(watcher);
} catch {
}
}, "addWatcher");
addWatcher(normalizedBasePath);
if (includeSubdirs) {
const subdirs = await getSubdirectories(normalizedBasePath, true);
for (const subdir of subdirs) {
addWatcher(subdir);
}
}
return () => {
for (const watcher of watchers) {
watcher.close();
}
};
}
__name(watchDirRecursive, "watchDirRecursive");
async function getDirStats(dirPath) {
let fileCount = 0;
let dirCount = 0;
let totalSize = 0;
async function processDir(currentPath) {
const entries = await fsp.readdir(currentPath, {
withFileTypes: true
});
for (const entry of entries) {
const entryPath = path.join(currentPath, entry.name);
if (entry.isDirectory()) {
dirCount++;
await processDir(entryPath);
} else if (entry.isFile()) {
fileCount++;
const stat = await fsp.stat(entryPath);
totalSize += stat.size;
}
}
}
__name(processDir, "processDir");
await processDir(dirPath);
return {
fileCount,
dirCount,
totalSize
};
}
__name(getDirStats, "getDirStats");
async function walkDir(dirPath, options) {
const traversalOrder = options.traversalOrder || "pre";
const entries = await fsp.readdir(dirPath, {
withFileTypes: true
});
for (const entry of entries) {
const entryPath = path.join(dirPath, entry.name);
const stats = await fsp.stat(entryPath);
const entryInfo = {
path: entryPath,
name: entry.name,
isDirectory: entry.isDirectory(),
stats
};
if (entry.isDirectory()) {
if (traversalOrder === "pre") {
const shouldContinue = await options.visitorFn(entryInfo);
if (shouldContinue !== false) {
await walkDir(entryPath, options);
}
} else {
await walkDir(entryPath, options);
await options.visitorFn(entryInfo);
}
} else {
await options.visitorFn(entryInfo);
}
}
if (traversalOrder === "post") {
const stats = await fsp.stat(dirPath);
const entryInfo = {
path: dirPath,
name: path.basename(dirPath),
isDirectory: true,
stats
};
await options.visitorFn(entryInfo);
}
}
__name(walkDir, "walkDir");
export { copyDir, createTempDir, deleteDirRecursive, emptyDir, ensureDir, ensureEmptyDir, findFilesByPattern, findInDir, findNewestFile, findOldestFile, getDirSize, getDirStats, getSubdirectories, isDirectory, listFiles, moveDir, walkDir, watchDir, watchDirRecursive };