|
'use strict';
|
|
|
|
const path = require('path');
|
|
const globToRegExp = require('glob-to-regexp');
|
|
|
|
module.exports = normalizeOptions;
|
|
|
|
let isWindows = /^win/.test(process.platform);
|
|
|
|
/**
|
|
* @typedef {Object} FSFacade
|
|
* @property {fs.readdir} readdir
|
|
* @property {fs.stat} stat
|
|
* @property {fs.lstat} lstat
|
|
*/
|
|
|
|
/**
|
|
* Validates and normalizes the options argument
|
|
*
|
|
* @param {object} [options] - User-specified options, if any
|
|
* @param {object} internalOptions - Internal options that aren't part of the public API
|
|
*
|
|
* @param {number|boolean|function} [options.deep]
|
|
* The number of directories to recursively traverse. Any falsy value or negative number will
|
|
* default to zero, so only the top-level contents will be returned. Set to `true` or `Infinity`
|
|
* to traverse all subdirectories. Or provide a function that accepts a {@link fs.Stats} object
|
|
* and returns a truthy value if the directory's contents should be crawled.
|
|
*
|
|
* @param {function|string|RegExp} [options.filter]
|
|
* A function that accepts a {@link fs.Stats} object and returns a truthy value if the data should
|
|
* be returned. Or a RegExp or glob string pattern, to filter by file name.
|
|
*
|
|
* @param {string} [options.sep]
|
|
* The path separator to use. By default, the OS-specific separator will be used, but this can be
|
|
* set to a specific value to ensure consistency across platforms.
|
|
*
|
|
* @param {string} [options.basePath]
|
|
* The base path to prepend to each result. If empty, then all results will be relative to `dir`.
|
|
*
|
|
* @param {FSFacade} [options.fs]
|
|
* Synchronous or asynchronous facades for Node.js File System module
|
|
*
|
|
* @param {object} [internalOptions.facade]
|
|
* Synchronous or asynchronous facades for various methods, including for the Node.js File System module
|
|
*
|
|
* @param {boolean} [internalOptions.emit]
|
|
* Indicates whether the reader should emit "file", "directory", and "symlink" events
|
|
*
|
|
* @param {boolean} [internalOptions.stats]
|
|
* Indicates whether the reader should emit {@link fs.Stats} objects instead of path strings
|
|
*
|
|
* @returns {object}
|
|
*/
|
|
function normalizeOptions (options, internalOptions) {
|
|
if (options === null || options === undefined) {
|
|
options = {};
|
|
}
|
|
else if (typeof options !== 'object') {
|
|
throw new TypeError('options must be an object');
|
|
}
|
|
|
|
let recurseDepth, recurseFn, recurseRegExp, recurseGlob, deep = options.deep;
|
|
if (deep === null || deep === undefined) {
|
|
recurseDepth = 0;
|
|
}
|
|
else if (typeof deep === 'boolean') {
|
|
recurseDepth = deep ? Infinity : 0;
|
|
}
|
|
else if (typeof deep === 'number') {
|
|
if (deep < 0 || isNaN(deep)) {
|
|
throw new Error('options.deep must be a positive number');
|
|
}
|
|
else if (Math.floor(deep) !== deep) {
|
|
throw new Error('options.deep must be an integer');
|
|
}
|
|
else {
|
|
recurseDepth = deep;
|
|
}
|
|
}
|
|
else if (typeof deep === 'function') {
|
|
recurseDepth = Infinity;
|
|
recurseFn = deep;
|
|
}
|
|
else if (deep instanceof RegExp) {
|
|
recurseDepth = Infinity;
|
|
recurseRegExp = deep;
|
|
}
|
|
else if (typeof deep === 'string' && deep.length > 0) {
|
|
recurseDepth = Infinity;
|
|
recurseGlob = globToRegExp(deep, { extended: true, globstar: true });
|
|
}
|
|
else {
|
|
throw new TypeError('options.deep must be a boolean, number, function, regular expression, or glob pattern');
|
|
}
|
|
|
|
let filterFn, filterRegExp, filterGlob, filter = options.filter;
|
|
if (filter !== null && filter !== undefined) {
|
|
if (typeof filter === 'function') {
|
|
filterFn = filter;
|
|
}
|
|
else if (filter instanceof RegExp) {
|
|
filterRegExp = filter;
|
|
}
|
|
else if (typeof filter === 'string' && filter.length > 0) {
|
|
filterGlob = globToRegExp(filter, { extended: true, globstar: true });
|
|
}
|
|
else {
|
|
throw new TypeError('options.filter must be a function, regular expression, or glob pattern');
|
|
}
|
|
}
|
|
|
|
let sep = options.sep;
|
|
if (sep === null || sep === undefined) {
|
|
sep = path.sep;
|
|
}
|
|
else if (typeof sep !== 'string') {
|
|
throw new TypeError('options.sep must be a string');
|
|
}
|
|
|
|
let basePath = options.basePath;
|
|
if (basePath === null || basePath === undefined) {
|
|
basePath = '';
|
|
}
|
|
else if (typeof basePath === 'string') {
|
|
// Append a path separator to the basePath, if necessary
|
|
if (basePath && basePath.substr(-1) !== sep) {
|
|
basePath += sep;
|
|
}
|
|
}
|
|
else {
|
|
throw new TypeError('options.basePath must be a string');
|
|
}
|
|
|
|
// Convert the basePath to POSIX (forward slashes)
|
|
// so that glob pattern matching works consistently, even on Windows
|
|
let posixBasePath = basePath;
|
|
if (posixBasePath && sep !== '/') {
|
|
posixBasePath = posixBasePath.replace(new RegExp('\\' + sep, 'g'), '/');
|
|
|
|
/* istanbul ignore if */
|
|
if (isWindows) {
|
|
// Convert Windows root paths (C:\) and UNCs (\\) to POSIX root paths
|
|
posixBasePath = posixBasePath.replace(/^([a-zA-Z]\:\/|\/\/)/, '/');
|
|
}
|
|
}
|
|
|
|
// Determine which facade methods to use
|
|
let facade;
|
|
if (options.fs === null || options.fs === undefined) {
|
|
// The user didn't provide their own facades, so use our internal ones
|
|
facade = internalOptions.facade;
|
|
}
|
|
else if (typeof options.fs === 'object') {
|
|
// Merge the internal facade methods with the user-provided `fs` facades
|
|
facade = Object.assign({}, internalOptions.facade);
|
|
facade.fs = Object.assign({}, internalOptions.facade.fs, options.fs);
|
|
}
|
|
else {
|
|
throw new TypeError('options.fs must be an object');
|
|
}
|
|
|
|
return {
|
|
recurseDepth,
|
|
recurseFn,
|
|
recurseRegExp,
|
|
recurseGlob,
|
|
filterFn,
|
|
filterRegExp,
|
|
filterGlob,
|
|
sep,
|
|
basePath,
|
|
posixBasePath,
|
|
facade,
|
|
emit: !!internalOptions.emit,
|
|
stats: !!internalOptions.stats,
|
|
};
|
|
}
|