scandirectory
Version:
Scan a directory recursively with a lot of control and power
301 lines (300 loc) • 11.4 kB
JavaScript
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;
;