You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

168 lines
5.9 KiB

4 years ago
  1. 'use strict';
  2. var fs = require('fs'),
  3. path = require('path'),
  4. defaults = require('lodash.defaults');
  5. var PACKAGE_NAME = require('../package.json').name;
  6. /**
  7. * Factory for find-file with the given <code>options</code> hash.
  8. * @param {{debug: boolean, attempts:number}} [opt] Optional options hash
  9. */
  10. function findFile(opt) {
  11. var options = defaults(opt, {
  12. debug: false,
  13. attempts: 0
  14. });
  15. return {
  16. absolute: absolute,
  17. base : base
  18. };
  19. /**
  20. * Search for the relative file reference from the <code>startPath</code> up to the process
  21. * working directory, avoiding any other directories with a <code>package.json</code> or <code>bower.json</code>.
  22. * @param {string} startPath The location of the uri declaration and the place to start the search from
  23. * @param {string} uri The content of the url() statement, expected to be a relative file path
  24. * @param {string} [limit] Optional directory to limit the search to
  25. * @returns {string|null} <code>null</code> where not found else the absolute path to the file
  26. */
  27. function absolute(startPath, uri, limit) {
  28. var basePath = base(startPath, uri, limit);
  29. return !!basePath && path.resolve(basePath, uri) || null;
  30. }
  31. /**
  32. * Search for the relative file reference from the <code>startPath</code> up to the process
  33. * working directory, avoiding any other directories with a <code>package.json</code> or <code>bower.json</code>.
  34. * @param {string} startPath The location of the uri declaration and the place to start the search from
  35. * @param {string} uri The content of the url() statement, expected to be a relative file path
  36. * @param {string} [limit] Optional directory to limit the search to
  37. * @returns {string|null} <code>null</code> where not found else the base path upon which the uri may be resolved
  38. */
  39. function base(startPath, uri, limit) {
  40. var messages = [];
  41. // ensure we have some limit to the search
  42. limit = limit && path.resolve(limit) || process.cwd();
  43. // #69 limit searching: make at least one attempt
  44. var remaining = Math.max(0, options.attempts) || 1E+9;
  45. // ignore explicit uris data|http|https and ensure we are at a valid start path
  46. var absoluteStart = !(/^(data|https?):/.test(uri)) && path.resolve(startPath);
  47. if (absoluteStart) {
  48. // find path to the root, stopping at cwd, package.json or bower.json
  49. var pathToRoot = [];
  50. var isWorking;
  51. do {
  52. pathToRoot.push(absoluteStart);
  53. isWorking = testWithinLimit(absoluteStart) && testNotPackage(absoluteStart);
  54. absoluteStart = path.resolve(absoluteStart, '..');
  55. } while (isWorking);
  56. // start a queue with the path to the root
  57. var appendLimit = options.includeRoot && pathToRoot.indexOf(limit) === -1 ? limit : [];
  58. var queue = pathToRoot.concat(appendLimit);
  59. // the queue pattern ensures that we favour paths closest the the start path
  60. // process the queue until empty or until we exhaust our attempts
  61. while (queue.length && (remaining-- > 0)) {
  62. // shift the first item off the queue, consider it the base for our relative uri
  63. var basePath = queue.shift();
  64. var fullPath = path.resolve(basePath, uri);
  65. messages.push(basePath);
  66. // file exists so convert to a dataURI and end
  67. if (fs.existsSync(fullPath)) {
  68. flushMessages('FOUND');
  69. return basePath;
  70. }
  71. // enqueue subdirectories that are not packages and are not in the root path
  72. else {
  73. enqueue(queue, basePath);
  74. }
  75. }
  76. // interrupted by options.attempts
  77. if (queue.length) {
  78. flushMessages('NOT FOUND (INTERRUPTED)');
  79. }
  80. // not found
  81. else {
  82. flushMessages('NOT FOUND');
  83. return null;
  84. }
  85. }
  86. // ignored
  87. else {
  88. flushMessages('IGNORED');
  89. return null;
  90. }
  91. /**
  92. * Enqueue subdirectories that are not packages and are not in the root path
  93. * @param {Array} queue The queue to add to
  94. * @param {string} basePath The path to consider
  95. */
  96. function enqueue(queue, basePath) {
  97. fs.readdirSync(basePath)
  98. .filter(function notHidden(filename) {
  99. return (filename.charAt(0) !== '.');
  100. })
  101. .map(function toAbsolute(filename) {
  102. return path.join(basePath, filename);
  103. })
  104. .filter(function directoriesOnly(absolutePath) {
  105. return fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory();
  106. })
  107. .filter(function notInRootPath(absolutePath) {
  108. return (pathToRoot.indexOf(absolutePath) < 0);
  109. })
  110. .filter(testNotPackage)
  111. .forEach(function enqueue(absolutePath) {
  112. queue.push(absolutePath);
  113. });
  114. }
  115. /**
  116. * Test whether the given directory is above but not equal to any of the project root directories.
  117. * @param {string} absolutePath An absolute path
  118. * @returns {boolean} True where a package.json or bower.json exists, else False
  119. */
  120. function testWithinLimit(absolutePath) {
  121. var relative = path.relative(limit, absolutePath);
  122. return !!relative && (relative.slice(0, 2) !== '..');
  123. }
  124. /**
  125. * Print verbose debug info where <code>options.debug</code> is in effect.
  126. * @param {string} result Final text to append to the message
  127. */
  128. function flushMessages(result) {
  129. if (options.debug) {
  130. var text = ['\n' + PACKAGE_NAME + ': ' + uri]
  131. .concat(messages)
  132. .concat(result)
  133. .join('\n ');
  134. console.log(text);
  135. }
  136. }
  137. }
  138. /**
  139. * Test whether the given directory is the root of its own package.
  140. * @param {string} absolutePath An absolute path
  141. * @returns {boolean} True where a package.json or bower.json exists, else False
  142. */
  143. function testNotPackage(absolutePath) {
  144. return ['package.json', 'bower.json'].every(function fileFound(file) {
  145. return !fs.existsSync(path.resolve(absolutePath, file));
  146. });
  147. }
  148. }
  149. module.exports = findFile;