@ 261106.3
current
'use strict';

const {
  ArrayIsArray,
  ArrayPrototypeMap,
  ArrayPrototypePush,
} = primordials;

const { lstatSync } = require('fs');
const { basename, dirname, isAbsolute, join, resolve } = require('path');

const {
  kEmptyObject,
  isWindows,
} = require('internal/util');
const {
  validateBoolean,
  validateInteger,
  validateObject,
  validateString,
  validateStringArray,
} = require('internal/validators');
const { Dirent, DirentFromStats } = require('internal/fs/utils');
const {
  codes: {
    ERR_INVALID_ARG_TYPE,
  },
  hideStackFrames,
} = require('internal/errors');
const { toPathIfFileURL } = require('internal/url');
const globBinding = internalBinding('glob');

// Flag bits defined by the native engine (src/glob/node_glob.cc), which
// applies the host platform's own bits itself.
const {
  kFlagWindows,
  kFlagDot,
  kFlagFollowSymlinks,
  kFlagWithFileTypes,
  kFlagMatchBase,
} = globBinding;

/**
 * @param {string} path
 * @returns {DirentFromStats|null}
 */
function getDirentSync(path) {
  let stat;
  try {
    stat = lstatSync(path);
  } catch {
    return null;
  }
  return new DirentFromStats(basename(path), stat, dirname(path));
}

/**
 * @callback validateStringArrayOrFunction
 * @param {*} value
 * @param {string} name
 */
const validateStringArrayOrFunction = hideStackFrames((value, name) => {
  if (ArrayIsArray(value)) {
    validateStringArray(value, name);
    return;
  }
  if (typeof value !== 'function') {
    throw new ERR_INVALID_ARG_TYPE(name, ['string[]', 'function'], value);
  }
});

class Glob {
  #root;
  #patterns;
  #excludePatterns;
  #excludeFn;
  #withFileTypes;
  #followSymlinks = false;
  #maxDepth = Infinity;

  constructor(pattern, options = kEmptyObject) {
    validateObject(options, 'options');
    const { exclude, cwd, followSymlinks, maxDepth, withFileTypes } = options;
    this.#root = toPathIfFileURL(cwd) ?? '.';
    validateString(this.#root, 'options.cwd');
    if (followSymlinks != null) {
      validateBoolean(followSymlinks, 'options.followSymlinks');
      this.#followSymlinks = followSymlinks;
    }
    if (maxDepth != null && maxDepth !== Infinity) {
      validateInteger(maxDepth, 'options.maxDepth', 0);
      this.#maxDepth = maxDepth;
    }
    this.#withFileTypes = !!withFileTypes;
    this.#excludePatterns = [];
    if (exclude != null) {
      validateStringArrayOrFunction(exclude, 'options.exclude');
      if (ArrayIsArray(exclude)) {
        // Exclude patterns are matched against absolute paths. Exclude
        // matching is a pure string comparison with no filesystem lookups, so
        // unlike include patterns, literal segments also match
        // case-insensitively on case-insensitive platforms (see native).
        this.#excludePatterns = ArrayPrototypeMap(
          exclude, (pattern) => resolve(this.#root, pattern));
      } else {
        this.#excludeFn = exclude;
      }
    }
    if (typeof pattern === 'object') {
      validateStringArray(pattern, 'patterns');
      this.#patterns = pattern;
    } else {
      validateString(pattern, 'patterns');
      this.#patterns = [pattern];
    }
  }

  // Whether any pattern is more than a literal path, see native.
  get hasMagic() {
    for (let i = 0; i < this.#patterns.length; i++) {
      if (globBinding.hasMagic(this.#patterns[i])) return true;
    }
    return false;
  }

  globSync() {
    const result = globBinding.globSync(
      this.#root, this.#patterns, this.#excludePatterns, this.#flags(),
      this.#excludeAdapter(), this.#maxDepth);
    if (!this.#withFileTypes) {
      return result;
    }
    const { 0: paths, 1: types } = result;
    const dirents = [];
    for (let i = 0; i < paths.length; i++) {
      ArrayPrototypePush(dirents, this.#toDirent(paths[i], types[i]));
    }
    return dirents;
  }

  async globAll() {
    const handle = globBinding.globStart(
      this.#root, this.#patterns, this.#excludePatterns, this.#flags(),
      this.#excludeAdapter(), this.#maxDepth);
    try {
      const { 0: paths, 1: types } = await handle.all();
      if (!this.#withFileTypes) {
        return paths;
      }
      const dirents = [];
      for (let i = 0; i < paths.length; i++) {
        ArrayPrototypePush(dirents, this.#toDirent(paths[i], types[i]));
      }
      return dirents;
    } finally {
      handle.cancel();
    }
  }

  // Pulls one batch of results at a time
  async * glob() {
    const handle = globBinding.globStart(
      this.#root, this.#patterns, this.#excludePatterns, this.#flags(),
      this.#excludeAdapter(), this.#maxDepth);
    try {
      let done = false;
      while (!done) {
        const { 0: paths, 1: types, 2: batchDone } = await handle.next();
        done = batchDone;
        for (let i = 0; i < paths.length; i++) {
          yield this.#withFileTypes ? this.#toDirent(paths[i], types[i]) : paths[i];
        }
      }
    } finally {
      handle.cancel();
    }
  }

  #flags() {
    return (this.#followSymlinks ? kFlagFollowSymlinks : 0) |
      (this.#withFileTypes ? kFlagWithFileTypes : 0);
  }

  // Shapes the argument an `exclude` callback receives to
  // work both with and without Dirents
  #excludeAdapter() {
    const exclude = this.#excludeFn;
    if (exclude === undefined) {
      return undefined;
    }
    if (!this.#withFileTypes) {
      return (entry, value) => exclude(value);
    }
    return (entry, value, parentPath, type) => {
      if (entry) {
        return exclude(new Dirent(value, type, parentPath));
      }
      const dirent = getDirentSync(value);
      return dirent === null ? false : exclude(dirent);
    };
  }

  #toDirent(path, type) {
    const full = isAbsolute(path) ? path : join(this.#root, path);
    return new Dirent(basename(full), type, dirname(full));
  }
}

/**
 * Check if a path matches a glob pattern
 * @param {string} path the path to check
 * @param {string} pattern the glob pattern to match
 * @param {boolean} windows whether the path is on a Windows system, defaults to `isWindows`
 * @param {boolean} dot whether wildcards may match dotfiles, defaults to `false`
 * @returns {boolean}
 */
function matchGlobPattern(path, pattern, windows = isWindows, dot = false) {
  validateString(path, 'path');
  validateString(pattern, 'pattern');
  return globBinding.matchesGlob(
    path, pattern, (windows ? kFlagWindows : 0) | (dot ? kFlagDot : 0));
}

/**
 * Check if a path matches a glob pattern, letting a pattern without a
 * separator match the path's basename
 * @param {string} path the path to check
 * @param {string} pattern the glob pattern to match
 * @returns {boolean}
 */
function matchGlobBasename(path, pattern) {
  return globBinding.matchesGlob(
    path, pattern, (isWindows ? kFlagWindows : 0) | kFlagMatchBase);
}

module.exports = {
  __proto__: null,
  Glob,
  matchGlobBasename,
  matchGlobPattern,
};