UNPKG

scandirectory

Version:

Scan a directory recursively with a lot of control and power

301 lines (300 loc) 11.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.stringify = exports.toTree = exports.toList = exports.scanDirectory = void 0; // builtin const path_1 = require("path"); const fs_1 = require("fs"); // external const ignorefs_1 = require("ignorefs"); /** Get the string or buffer of a file */ async function readHelper(path, opts) { return new Promise(function (resolve, reject) { (0, fs_1.readFile)(path, opts, function (err, data) { if (err) reject(err); else resolve(data); }); }); } /** Get the stat */ async function statsHelper(path) { return new Promise(function (resolve, reject) { (0, fs_1.stat)(path, function (err, stats) { if (err) reject(err); else resolve(stats); }); }); } /** Get a list of files */ async function listHelper(directory) { return new Promise(function (resolve, reject) { (0, fs_1.readdir)(directory, function (err, files) { if (err) reject(err); else resolve(files); }); }); } /** Scan the contents of a directory */ async function scanDirectory(opts) { // options opts = Object.assign({}, opts); if (opts.includeRoot == null) opts.includeRoot = false; if (opts.recurse == null) opts.recurse = true; if (opts.fileAction == null) opts.fileAction = opts.action; if (opts.dirAction == null) opts.dirAction = opts.action; // prepare const pendingLists = [ { absolutePath: (0, path_1.resolve)(opts.directory), relativePath: '.', basename: (0, path_1.basename)(opts.directory), directory: true, stats: await statsHelper(opts.directory), parent: null, children: null, data: null, }, ]; const pendingStats = []; const pendingReads = []; const results = {}; if (opts.includeRoot) results['.'] = pendingLists[0]; // add subsequent const abort = typeof AbortController !== 'undefined' ? new AbortController() : null; // v14.7+ const signal = abort?.signal; try { while (pendingLists.length || pendingStats.length || pendingReads.length) { await Promise.all([ ...pendingLists .splice(0, pendingLists.length) .map(async function (result) { const files = (await listHelper(result.absolutePath)).sort(); result.children = {}; for (const basename of files) { const child = { absolutePath: (0, path_1.join)(result.absolutePath, basename), relativePath: (0, path_1.join)(result.relativePath, basename), basename, directory: null, stats: null, parent: result, children: null, data: null, }; result.children[child.basename] = child; pendingStats.push(child); } }), ...pendingStats .splice(0, pendingStats.length) .map(async function (wipResult) { const stats = await statsHelper(wipResult.absolutePath); const directory = stats.isDirectory(); const result = Object.assign(wipResult, { directory, stats, parent: wipResult.parent, children: null, data: null, }); // skip by ignore options? if ((0, ignorefs_1.isIgnoredPath)(result, opts)) { if (result.parent) delete result.parent.children[result.basename]; return; } // dir or file if (result.directory) { // skip by dir action? const skip = opts.dirAction === false || (opts.dirAction && opts.dirAction(result) === false); if (skip) { if (result.parent) delete result.parent.children[result.basename]; return; } // save results[result.relativePath] = result; if (opts.recurse) pendingLists.push(result); } else { // skip by file action? const skip = opts.fileAction === false || (opts.fileAction && opts.fileAction(result) === false); if (skip) { if (result.parent) delete result.parent.children[result.basename]; return; } // save results[result.relativePath] = result; if (opts.encoding != null) pendingReads.push(result); } }), ...pendingReads .splice(0, pendingReads.length) .map(async function (result) { if (opts.encoding === 'binary') { ; result.data = await readHelper(result.absolutePath, { encoding: 'binary', signal, }); } else if (opts.encoding != null) { ; result.data = (await readHelper(result.absolutePath, { encoding: opts.encoding, signal, })); } }), ]); } // sort const sortedResults = {}; Object.keys(results) .sort() .forEach(function (key) { sortedResults[key] = results[key]; }); return sortedResults; } catch (err) { abort?.abort(); throw err; } } exports.scanDirectory = scanDirectory; /** Scan the contents of a directory, with compatibility for scandirectory < v8 */ async function scanDirectoryCompatibility(...args) { const opts = {}; try { // parse arguments into options args.forEach(function (arg) { switch (typeof arg) { case 'string': opts.directory = arg; break; case 'function': opts.next = arg; break; case 'object': Object.assign(opts, arg); break; default: throw new Error('scandirectory: unknown argument: ' + JSON.stringify(arg)); } }); // handle deprecations and verifications if (opts.next) opts.includeRoot = true; if (opts.path != null) opts.directory = opts.path; if (opts.readFiles) { throw new Error('scandirectory: readFiles renamed to encoding: if you used readFiles = true, use encoding = "utf8", if you used readFiles = "binary", use encoding = "binary"'); } if (!opts.directory) { throw new Error('scandirectory: path is needed'); } // action callback compatibility if (opts.action && opts.action.length >= 1) { const actionCallback = opts.action; opts.action = function (result) { return actionCallback(result.absolutePath, result.relativePath, result.basename, Object.assign({ directory: result.directory }, result.stats)); }; } if (opts.dirAction && opts.dirAction.length >= 1) { const dirActionCallback = opts.dirAction; opts.dirAction = function (result) { return dirActionCallback(result.absolutePath, result.relativePath, result.basename, Object.assign({ directory: result.directory }, result.stats)); }; } if (opts.fileAction && opts.fileAction.length >= 1) { const fileActionCallback = opts.fileAction; opts.fileAction = function (result) { return fileActionCallback(result.absolutePath, result.relativePath, result.basename, Object.assign({ directory: result.directory }, result.stats)); }; } // upgrade options and fetch results from modern api const results = await scanDirectory((0, ignorefs_1.upgradeOptions)(opts)); if (opts.next) { if (opts.next.length === 1) { opts.next(null); } else { const list = toList(results); if (opts.next.length === 2) { opts.next(null, list); } else if (opts.next.length === 3) { const tree = toTree(results); opts.next(null, list, tree); } else throw new Error('scandirectory: next function must accept 1, 2, or 3 arguments'); } } return results; } catch (err) { if (opts.next) { opts.next(err); return {}; } else { throw err; } } } exports.default = scanDirectoryCompatibility; /** Compatibility helper for {@link scanDirectoryCompatibility} to generate results compatible with {@link CompatibilityNextCallback} */ function toList(results) { const list = {}; // Object.entries requires Node.js >= 8 for (const key of Object.keys(results)) { const value = results[key]; list[key] = (value.directory ? 'dir' : value.data ?? 'file'); } delete list['.']; return list; } exports.toList = toList; /** Compatibility helper for {@link scanDirectoryCompatibility} to generate results compatible with {@link CompatibilityNextCallback} */ function toTree(results, descending = false) { if (!results) return {}; const tree = {}; if (!descending) { return toTree(results['.'].children, true); } // Object.entries requires Node.js >= 8 for (const key of Object.keys(results)) { const value = results[key]; tree[key] = (value.directory ? toTree(value.children, descending) : value.data ?? true); } return tree; } exports.toTree = toTree; /** Convert {@link Results} into a non-recursive JSON string */ function stringify(any, indentation) { return JSON.stringify(any, (key, value) => { if (key === 'parent') return value.relativePath; return value; }, indentation); } exports.stringify = stringify;