ManifestSearch.js

const glob = require("glob-promise");

/**
 * Options to configure the operation of {@link ManifestSearch}
 * @typedef {Object} ManifestSearchOptions
 * @property {Object} glob [Options]{@link https://github.com/isaacs/node-glob#options} to pass to the
 *   [glob]{@link https://github.com/isaacs/node-glob} library.
 * @property {String} glob.cwd The current working directory in which to search. Defaults to `process.cwd()`.
 * @property {Array<String>} glob.ignore Add a pattern or an array of glob patterns to exclude matches.
 *   Set to `["**\/node_modules/**\/*"]` by default.
 * @property {String} query The query to pass to Glob.  If not explicitly provided, a default query will be built
 *   according to other options.
 * @property {Boolean} deep Sets the default `query`.  If `true`, searches directories recursively for manifest files.
 *   If `false`, only one layer of directories is searched.  Defaults to `false`.
 * @property {Array<String>} manifestFiles The filenames for manifest files to search for.  `"*"` is allowed according
 *   to the Glob library.  Defaults to `["package.json"]`.
 * @property {Array<SearchResult>} include Specific search results to include in the output.  Useful to add a single
 *   file in a larger directory excluded by `glob.ignore`.
*/

/**
 * The path to a found manifest.
 * @typedef {!String} SearchResult
*/

/**
 * All results from a package search.
 * @typedef {!Array<SearchResult>} SearchResults
*/

/**
 * Searches for Manifest files (`package.json`)
*/
class ManifestSearch {

  /**
   * @param {ManifestSearchOptions} [opts] Options to configure searching.
  */
  constructor(opts) {
    /**
     * The current options used for searching.
     * @type {ManifestSearchOptions}
    */
    this.opts = opts || {};
    if(!this.opts.glob) { this.opts.glob = {}; }
    if(!this.opts.glob.ignore) { this.opts.glob.ignore = ["**/node_modules/**/*"]; }
    if(!this.opts.include) { this.opts.include = []; }
    /**
     * The last search executed, cached for preformance.
     * @type {Promise<SearchResults>}
     * @private
    */
    this._lastSearch = null;
  }

  /**
   * Exclude a specific Glob pattern from being searched.
   * @param {String} pattern the Glob pattern to add.
   * @return {ManifestSearch} for chaining.
  */
  exclude(pattern) {
    this.opts.glob.ignore.push(pattern);
    return this;
  }

  /**
   * Add a search result to explicitly be included in the output.
   * @param {SearchResult} result the result to include in output.
   * @return {ManifestSearch} for chaining.
  */
  include(result) {
    this.opts.include.push(result);
    return this;
  }

  /**
   * Start a search for manifests.
   * @param {Boolean} cache if `true`, returns the last search (if available) instead of searching again.
   * @return {Promise<SearchResults>}
  */
  search(cache) {
    if(cache && this._lastSearch) { return this._lastSearch; }
    let manifestFiles = "@(" + (this.opts.manifestFiles || ["package.json"]).join(",") + ")";
    let query = this.opts.query || ((this.opts.deep || false) ? "**/" + manifestFiles : "*/" + manifestFiles);
    return glob
      .promise(query, this.opts.glob)
      .then( (contents) => contents.concat(this.opts.include) );
  }

}

module.exports = ManifestSearch;