lookup-files.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. 'use strict';
  2. /**
  3. * Contains `lookupFiles`, which takes some globs/dirs/options and returns a list of files.
  4. * @module
  5. * @private
  6. */
  7. var fs = require('node:fs');
  8. var path = require('node:path');
  9. var glob = require('glob');
  10. var errors = require('../errors');
  11. var createNoFilesMatchPatternError = errors.createNoFilesMatchPatternError;
  12. var createMissingArgumentError = errors.createMissingArgumentError;
  13. const debug = require('debug')('mocha:cli:lookup-files');
  14. /**
  15. * Determines if pathname would be a "hidden" file (or directory) on UN*X.
  16. *
  17. * @description
  18. * On UN*X, pathnames beginning with a full stop (aka dot) are hidden during
  19. * typical usage. Dotfiles, plain-text configuration files, are prime examples.
  20. *
  21. * @see {@link http://xahlee.info/UnixResource_dir/writ/unix_origin_of_dot_filename.html|Origin of Dot File Names}
  22. *
  23. * @private
  24. * @param {string} pathname - Pathname to check for match.
  25. * @return {boolean} whether pathname would be considered a hidden file.
  26. * @example
  27. * isHiddenOnUnix('.profile'); // => true
  28. */
  29. const isHiddenOnUnix = pathname => path.basename(pathname).startsWith('.');
  30. /**
  31. * Determines if pathname has a matching file extension.
  32. *
  33. * Supports multi-part extensions.
  34. *
  35. * @private
  36. * @param {string} pathname - Pathname to check for match.
  37. * @param {string[]} exts - List of file extensions, w/-or-w/o leading period
  38. * @return {boolean} `true` if file extension matches.
  39. * @example
  40. * hasMatchingExtname('foo.html', ['js', 'css']); // false
  41. * hasMatchingExtname('foo.js', ['.js']); // true
  42. * hasMatchingExtname('foo.js', ['js']); // ture
  43. */
  44. const hasMatchingExtname = (pathname, exts = []) =>
  45. exts
  46. .map(ext => (ext.startsWith('.') ? ext : `.${ext}`))
  47. .some(ext => pathname.endsWith(ext));
  48. /**
  49. * Lookup file names at the given `path`.
  50. *
  51. * @description
  52. * Filenames are returned in _traversal_ order by the OS/filesystem.
  53. * **Make no assumption that the names will be sorted in any fashion.**
  54. *
  55. * @public
  56. * @alias module:lib/cli.lookupFiles
  57. * @param {string} filepath - Base path to start searching from.
  58. * @param {string[]} [extensions=[]] - File extensions to look for.
  59. * @param {boolean} [recursive=false] - Whether to recurse into subdirectories.
  60. * @return {string[]} An array of paths.
  61. * @throws {Error} if no files match pattern.
  62. * @throws {TypeError} if `filepath` is directory and `extensions` not provided.
  63. */
  64. module.exports = function lookupFiles(
  65. filepath,
  66. extensions = [],
  67. recursive = false
  68. ) {
  69. const files = [];
  70. let stat;
  71. if (!fs.existsSync(filepath)) {
  72. let pattern;
  73. if (glob.hasMagic(filepath, {windowsPathsNoEscape: true})) {
  74. // Handle glob as is without extensions
  75. pattern = filepath;
  76. } else {
  77. // glob pattern e.g. 'filepath+(.js|.ts)'
  78. const strExtensions = extensions
  79. .map(ext => (ext.startsWith('.') ? ext : `.${ext}`))
  80. .join('|');
  81. pattern = `${filepath}+(${strExtensions})`;
  82. debug('looking for files using glob pattern: %s', pattern);
  83. }
  84. files.push(
  85. ...glob
  86. .sync(pattern, {
  87. nodir: true,
  88. windowsPathsNoEscape: true
  89. })
  90. // glob@8 and earlier sorted results in en; glob@9 depends on OS sorting.
  91. // This preserves the older glob behavior.
  92. // https://github.com/mochajs/mocha/pull/5250/files#r1840469747
  93. .sort((a, b) => a.localeCompare(b, 'en'))
  94. );
  95. if (!files.length) {
  96. throw createNoFilesMatchPatternError(
  97. `Cannot find any files matching pattern "${filepath}"`,
  98. filepath
  99. );
  100. }
  101. return files;
  102. }
  103. // Handle file
  104. try {
  105. stat = fs.statSync(filepath);
  106. if (stat.isFile()) {
  107. return filepath;
  108. }
  109. } catch (err) {
  110. // ignore error
  111. return;
  112. }
  113. // Handle directory
  114. fs.readdirSync(filepath).forEach(dirent => {
  115. const pathname = path.join(filepath, dirent);
  116. let stat;
  117. try {
  118. stat = fs.statSync(pathname);
  119. if (stat.isDirectory()) {
  120. if (recursive) {
  121. files.push(...lookupFiles(pathname, extensions, recursive));
  122. }
  123. return;
  124. }
  125. } catch (ignored) {
  126. return;
  127. }
  128. if (!extensions.length) {
  129. throw createMissingArgumentError(
  130. `Argument '${extensions}' required when argument '${filepath}' is a directory`,
  131. 'extensions',
  132. 'array'
  133. );
  134. }
  135. if (
  136. !stat.isFile() ||
  137. !hasMatchingExtname(pathname, extensions) ||
  138. isHiddenOnUnix(pathname)
  139. ) {
  140. return;
  141. }
  142. files.push(pathname);
  143. });
  144. return files;
  145. };