'use strict'; var fs = require('fs'), path = require('path'), defaults = require('lodash.defaults'); var PACKAGE_NAME = require('../package.json').name; /** * Factory for find-file with the given options hash. * @param {{debug: boolean, attempts:number}} [opt] Optional options hash */ function findFile(opt) { var options = defaults(opt, { debug: false, attempts: 0 }); return { absolute: absolute, base : base }; /** * Search for the relative file reference from the startPath up to the process * working directory, avoiding any other directories with a package.json or bower.json. * @param {string} startPath The location of the uri declaration and the place to start the search from * @param {string} uri The content of the url() statement, expected to be a relative file path * @param {string} [limit] Optional directory to limit the search to * @returns {string|null} null where not found else the absolute path to the file */ function absolute(startPath, uri, limit) { var basePath = base(startPath, uri, limit); return !!basePath && path.resolve(basePath, uri) || null; } /** * Search for the relative file reference from the startPath up to the process * working directory, avoiding any other directories with a package.json or bower.json. * @param {string} startPath The location of the uri declaration and the place to start the search from * @param {string} uri The content of the url() statement, expected to be a relative file path * @param {string} [limit] Optional directory to limit the search to * @returns {string|null} null where not found else the base path upon which the uri may be resolved */ function base(startPath, uri, limit) { var messages = []; // ensure we have some limit to the search limit = limit && path.resolve(limit) || process.cwd(); // #69 limit searching: make at least one attempt var remaining = Math.max(0, options.attempts) || 1E+9; // ignore explicit uris data|http|https and ensure we are at a valid start path var absoluteStart = !(/^(data|https?):/.test(uri)) && path.resolve(startPath); if (absoluteStart) { // find path to the root, stopping at cwd, package.json or bower.json var pathToRoot = []; var isWorking; do { pathToRoot.push(absoluteStart); isWorking = testWithinLimit(absoluteStart) && testNotPackage(absoluteStart); absoluteStart = path.resolve(absoluteStart, '..'); } while (isWorking); // start a queue with the path to the root var appendLimit = options.includeRoot && pathToRoot.indexOf(limit) === -1 ? limit : []; var queue = pathToRoot.concat(appendLimit); // the queue pattern ensures that we favour paths closest the the start path // process the queue until empty or until we exhaust our attempts while (queue.length && (remaining-- > 0)) { // shift the first item off the queue, consider it the base for our relative uri var basePath = queue.shift(); var fullPath = path.resolve(basePath, uri); messages.push(basePath); // file exists so convert to a dataURI and end if (fs.existsSync(fullPath)) { flushMessages('FOUND'); return basePath; } // enqueue subdirectories that are not packages and are not in the root path else { enqueue(queue, basePath); } } // interrupted by options.attempts if (queue.length) { flushMessages('NOT FOUND (INTERRUPTED)'); } // not found else { flushMessages('NOT FOUND'); return null; } } // ignored else { flushMessages('IGNORED'); return null; } /** * Enqueue subdirectories that are not packages and are not in the root path * @param {Array} queue The queue to add to * @param {string} basePath The path to consider */ function enqueue(queue, basePath) { fs.readdirSync(basePath) .filter(function notHidden(filename) { return (filename.charAt(0) !== '.'); }) .map(function toAbsolute(filename) { return path.join(basePath, filename); }) .filter(function directoriesOnly(absolutePath) { return fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory(); }) .filter(function notInRootPath(absolutePath) { return (pathToRoot.indexOf(absolutePath) < 0); }) .filter(testNotPackage) .forEach(function enqueue(absolutePath) { queue.push(absolutePath); }); } /** * Test whether the given directory is above but not equal to any of the project root directories. * @param {string} absolutePath An absolute path * @returns {boolean} True where a package.json or bower.json exists, else False */ function testWithinLimit(absolutePath) { var relative = path.relative(limit, absolutePath); return !!relative && (relative.slice(0, 2) !== '..'); } /** * Print verbose debug info where options.debug is in effect. * @param {string} result Final text to append to the message */ function flushMessages(result) { if (options.debug) { var text = ['\n' + PACKAGE_NAME + ': ' + uri] .concat(messages) .concat(result) .join('\n '); console.log(text); } } } /** * Test whether the given directory is the root of its own package. * @param {string} absolutePath An absolute path * @returns {boolean} True where a package.json or bower.json exists, else False */ function testNotPackage(absolutePath) { return ['package.json', 'bower.json'].every(function fileFound(file) { return !fs.existsSync(path.resolve(absolutePath, file)); }); } } module.exports = findFile;