146 lines
4.9 KiB
JavaScript
146 lines
4.9 KiB
JavaScript
import fs from 'node:fs';
|
|
import path, { join, resolve, relative, dirname } from 'node:path';
|
|
import process from 'node:process';
|
|
import { convertIgnorePatternToMinimatch } from '@eslint/compat';
|
|
import 'node:fs/promises';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
function toArray(array) {
|
|
array = array ?? [];
|
|
return Array.isArray(array) ? array : [array];
|
|
}
|
|
|
|
const toPath = urlOrPath => urlOrPath instanceof URL ? fileURLToPath(urlOrPath) : urlOrPath;
|
|
|
|
function findUpSync(name, {
|
|
cwd = process.cwd(),
|
|
type = 'file',
|
|
stopAt,
|
|
} = {}) {
|
|
let directory = path.resolve(toPath(cwd) ?? '');
|
|
const {root} = path.parse(directory);
|
|
stopAt = path.resolve(directory, toPath(stopAt) ?? root);
|
|
const isAbsoluteName = path.isAbsolute(name);
|
|
|
|
while (directory) {
|
|
const filePath = isAbsoluteName ? name : path.join(directory, name);
|
|
|
|
try {
|
|
const stats = fs.statSync(filePath, {throwIfNoEntry: false});
|
|
if ((type === 'file' && stats?.isFile()) || (type === 'directory' && stats?.isDirectory())) {
|
|
return filePath;
|
|
}
|
|
} catch {}
|
|
|
|
if (directory === stopAt || directory === root) {
|
|
break;
|
|
}
|
|
|
|
directory = path.dirname(directory);
|
|
}
|
|
}
|
|
|
|
const GITIGNORE = ".gitignore";
|
|
const GITMODULES = ".gitmodules";
|
|
const RE_NEWLINE = /\r?\n/u;
|
|
const RE_PARENT_PATH = /^(?:\.\.\/)+$/;
|
|
const RE_PATH_SEP = /[/\\]/;
|
|
const RE_SUBMODULE_PATH = /path\s*=\s*(.+)/u;
|
|
function ignore(options = {}) {
|
|
const ignores = [];
|
|
const {
|
|
cwd = process.cwd(),
|
|
root = false,
|
|
recursive = false,
|
|
files: _files = root ? GITIGNORE : findUpSync(GITIGNORE, { cwd }) || [],
|
|
filesGitModules: _filesGitModules = root ? fs.existsSync(join(cwd, GITMODULES)) ? GITMODULES : [] : findUpSync(GITMODULES, { cwd }) || [],
|
|
strict = true
|
|
} = options;
|
|
const files = new Set(toArray(_files).map((file) => resolve(cwd, file)));
|
|
if (recursive) {
|
|
const additionalIgnoreDirs = typeof recursive === "boolean" ? [] : recursive.skipDirs;
|
|
for (const file of findNestedFiles(cwd, GITIGNORE, additionalIgnoreDirs))
|
|
files.add(file);
|
|
}
|
|
const filesList = [...files];
|
|
const filesGitModules = toArray(_filesGitModules).map((file) => resolve(cwd, file));
|
|
for (const file of filesList) {
|
|
let content = "";
|
|
try {
|
|
content = fs.readFileSync(file, "utf8");
|
|
} catch (error) {
|
|
if (strict)
|
|
throw error;
|
|
continue;
|
|
}
|
|
const relativePath = relative(cwd, dirname(file)).replaceAll("\\", "/");
|
|
const globs = content.split(RE_NEWLINE).filter((line) => line && !line.startsWith("#")).map((line) => convertIgnorePatternToMinimatch(line)).map((glob) => relativeMinimatch(glob, relativePath, cwd)).filter((glob) => glob !== null);
|
|
ignores.push(...globs);
|
|
}
|
|
for (const file of filesGitModules) {
|
|
let content = "";
|
|
try {
|
|
content = fs.readFileSync(file, "utf8");
|
|
} catch (error) {
|
|
if (strict)
|
|
throw error;
|
|
continue;
|
|
}
|
|
const dirs = parseGitSubmodules(content);
|
|
ignores.push(...dirs.map((dir) => `${dir}/**`));
|
|
}
|
|
if (strict && filesList.length === 0)
|
|
throw new Error("No .gitignore file found");
|
|
return {
|
|
name: options.name || "gitignore",
|
|
ignores
|
|
};
|
|
}
|
|
function findNestedFiles(cwd, fileName, additionalIgnoreDirs) {
|
|
const files = [];
|
|
const directoriesToSkip = /* @__PURE__ */ new Set([".git", "node_modules", ...additionalIgnoreDirs]);
|
|
const queue = [cwd];
|
|
while (queue.length) {
|
|
const directory = queue.shift();
|
|
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const absolutePath = join(directory, entry.name);
|
|
if (entry.isFile() && entry.name === fileName)
|
|
files.push(absolutePath);
|
|
if (entry.isDirectory() && !directoriesToSkip.has(entry.name))
|
|
queue.push(absolutePath);
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
function relativeMinimatch(pattern, relativePath, cwd) {
|
|
if (["", ".", "/"].includes(relativePath))
|
|
return pattern;
|
|
const negated = pattern.startsWith("!") ? "!" : "";
|
|
let cleanPattern = negated ? pattern.slice(1) : pattern;
|
|
if (!relativePath.endsWith("/"))
|
|
relativePath = `${relativePath}/`;
|
|
const isParent = relativePath.startsWith("..");
|
|
if (!isParent)
|
|
return `${negated}${relativePath}${cleanPattern}`;
|
|
if (!RE_PARENT_PATH.test(relativePath))
|
|
throw new Error("The ignore file location should be either a parent or child directory");
|
|
if (cleanPattern.startsWith("**"))
|
|
return pattern;
|
|
const parents = relative(resolve(cwd, relativePath), cwd).split(RE_PATH_SEP);
|
|
while (parents.length && cleanPattern.startsWith(`${parents[0]}/`)) {
|
|
cleanPattern = cleanPattern.slice(parents[0].length + 1);
|
|
parents.shift();
|
|
}
|
|
if (cleanPattern.startsWith("**"))
|
|
return `${negated}${cleanPattern}`;
|
|
if (parents.length === 0)
|
|
return `${negated}${cleanPattern}`;
|
|
return null;
|
|
}
|
|
function parseGitSubmodules(content) {
|
|
return content.split(RE_NEWLINE).map((line) => RE_SUBMODULE_PATH.exec(line)).filter((match) => match !== null).map((match) => match[1].trim());
|
|
}
|
|
|
|
export { ignore as default };
|