'use strict'; const fs = require('fs'); const path = require('path'); class File { constructor(relpath, posixRelpath) { this.path = relpath; this.posixPath = posixRelpath; } error(message, details) { if (!Array.isArray(this.errors)) { this.errors = []; } this.errors.push({ message: String(message), details }); } } class Symlink { constructor(relpath, posixRelpath, relRealpath) { this.path = relpath; this.posixPath = posixRelpath; this.realpath = relRealpath; } } function scanErrorToJSON() { return { reason: this.reason, path: this.path, message: String(this) }; } function scanError(reason, path2, error) { error.reason = reason; error.path = path2; error.toJSON = scanErrorToJSON; return error; } function ensureArray(value) { if (Array.isArray(value)) { return value; } return value ? [value] : []; } function isRegExp(value) { return value instanceof RegExp; } function isString(value) { return typeof value === "string"; } function composeAccept(first, second) { return first ? (relpath) => first(relpath) && second(relpath) : second; } function posixNormalize(relpath) { return path.posix.resolve("/", relpath).slice(1); } function ensureEndsWithSep(value, sep) { return value && !value.endsWith(sep) ? value + sep : value; } function normalizeOptions(options = {}) { if (typeof options === "string") { options = { basedir: options }; } const basedir = path.resolve(options.basedir || process.cwd()); const resolveSymlinks = Boolean(options.resolveSymlinks); const generalInclude = new Set(ensureArray(options.include).map(posixNormalize)); const generalExclude = new Set(ensureArray(options.exclude).map(posixNormalize)); const onError = typeof options.onError === "function" ? options.onError : () => { }; const rawRules = ensureArray(options.rules).filter(Boolean); const onlyRule = rawRules.find((rule) => rule.only); const rules = (onlyRule ? [onlyRule] : rawRules.length ? rawRules : [{}]).map((rule) => { let test = null; let include = null; let exclude = null; let encoding = rule.encoding; let extract = null; let accept = null; if (rule.test) { const ruleTest = ensureArray(rule.test).slice(); if (!ruleTest.every(isRegExp)) { throw new Error("rule.test should be a RegExp or array of RegExp"); } test = ruleTest; accept = (posixRelpath) => { for (const rx of ruleTest) { if (rx.test(posixRelpath)) { return true; } } return false; }; } if (rule.include) { const ruleInclude = ensureArray(rule.include); if (!ruleInclude.every(isString)) { throw new Error("rule.include should be a string or array of strings"); } include = ruleInclude.map(posixNormalize); accept = composeAccept(accept, (posixRelpath) => { for (const dir of ruleInclude) { if (posixRelpath === dir || posixRelpath.startsWith(dir + "/")) { return true; } } return false; }); for (let dir of ruleInclude) { while (dir !== "" && dir !== ".") { generalInclude.add(dir); dir = path.dirname(dir); } } } if (rule.exclude) { const ruleExclude = ensureArray(rule.exclude); if (!ruleExclude.every(isString)) { throw new Error("rule.exclude should be a string or array of strings"); } exclude = ruleExclude.map(posixNormalize); accept = composeAccept(accept, (posixRelpath) => { for (const dir of ruleExclude) { if (posixRelpath === dir || posixRelpath.startsWith(dir + "/")) { return false; } } return true; }); } if (typeof rule.extract === "function") { extract = rule.extract; } else if (rule.extract) { throw new Error("rule.extract should be a function"); } if (typeof encoding !== "string" && encoding !== null) { encoding = "utf8"; } return Object.freeze({ basedir, accept, extract, encoding, config: rule, test, include, exclude }); }); generalInclude.forEach((dir) => generalExclude.delete(dir)); return { basedir, include: [...generalInclude], exclude: [...generalExclude], rules, resolveSymlinks, onError }; } async function scanFs(options) { async function collect(basedir2, reldir, posixReldir) { const tasks = []; for (const dirent of await fs.promises.readdir(basedir2 + reldir, { withFileTypes: true })) { const relpath = reldir + dirent.name; const posixRelpath = posixReldir + dirent.name; pathsScanned++; if (exclude.includes(posixRelpath)) { continue; } if (dirent.isDirectory()) { tasks.push(collect(basedir2, relpath + path.sep, posixRelpath + "/")); continue; } if (dirent.isSymbolicLink()) { const symlink = new Symlink(relpath, posixRelpath, null); symlinks.push(symlink); if (resolveSymlinks) { tasks.push(fs.promises.realpath(basedir2 + relpath).then((realpath) => { symlink.realpath = path.relative(basedir2, realpath); }).catch((error) => { errors.push(error = scanError("resolve-symlink", relpath, error)); onError(error); })); } continue; } filesTested++; for (const rule of rules) { const { accept, extract } = rule; if (accept && !accept(posixRelpath)) { continue; } const file = new File(relpath, posixRelpath); files.push(file); if (extract !== null) { tasks.push(fs.promises.readFile(basedir2 + relpath, rule.encoding).then((content) => extract(file, content, rule)).catch((error) => { errors.push(error = scanError("extract", relpath, error)); onError(error); })); } break; } } if (tasks.length > 0) { await Promise.all(tasks); } } const files = []; const symlinks = []; const errors = []; const { basedir, include, exclude, rules, resolveSymlinks, onError } = normalizeOptions(options); let pathsScanned = 0; let filesTested = 0; if (!include.length) { include.push(""); } else { exclude.push(...include); } await Promise.all(include.map((posixRelpath) => { return collect(ensureEndsWithSep(basedir, path.sep), ensureEndsWithSep(posixRelpath, "/").replace(/\//g, path.sep), ensureEndsWithSep(posixRelpath, "/")); })); return { basedir, files, symlinks, errors, pathsScanned, filesTested }; } exports.File = File; exports.Symlink = Symlink; exports.normalizeOptions = normalizeOptions; exports.scanFs = scanFs;