UNPKG

scandirectory

Version:

Scan a directory recursively with a lot of control and power

295 lines (294 loc) 11.3 kB
// builtin import { join, resolve, basename as getBasename } from 'path'; import { readdir as readdirBuiltin, stat as statBuiltin, readFile as readFileBuiltin, } from 'fs'; // external import { isIgnoredPath, upgradeOptions, } from 'ignorefs'; /** Get the string or buffer of a file */ async function readHelper(path, opts) { return new Promise(function (resolve, reject) { readFileBuiltin(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) { statBuiltin(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) { readdirBuiltin(directory, function (err, files) { if (err) reject(err); else resolve(files); }); }); } /** Scan the contents of a directory */ export 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: resolve(opts.directory), relativePath: '.', basename: getBasename(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 === null || abort === void 0 ? void 0 : 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: join(result.absolutePath, basename), relativePath: 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 (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 === null || abort === void 0 ? void 0 : abort.abort(); throw err; } } /** Scan the contents of a directory, with compatibility for scandirectory < v8 */ export default 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(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; } } } /** Compatibility helper for {@link scanDirectoryCompatibility} to generate results compatible with {@link CompatibilityNextCallback} */ export function toList(results) { var _a; const list = {}; // Object.entries requires Node.js >= 8 for (const key of Object.keys(results)) { const value = results[key]; list[key] = (value.directory ? 'dir' : (_a = value.data) !== null && _a !== void 0 ? _a : 'file'); } delete list['.']; return list; } /** Compatibility helper for {@link scanDirectoryCompatibility} to generate results compatible with {@link CompatibilityNextCallback} */ export function toTree(results, descending = false) { var _a; 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) : (_a = value.data) !== null && _a !== void 0 ? _a : true); } return tree; } /** Convert {@link Results} into a non-recursive JSON string */ export function stringify(any, indentation) { return JSON.stringify(any, (key, value) => { if (key === 'parent') return value.relativePath; return value; }, indentation); }