dependency-cruiser
Version:
Validate and visualize dependencies. With your rules. JavaScript, TypeScript, CoffeeScript. ES6, CommonJS, AMD.
283 lines (252 loc) • 7.13 kB
JavaScript
import { readdirSync, statSync, readFileSync } from "node:fs";
import { join, relative } from "node:path/posix";
import ignore from "ignore";
import pathToPosix from "./path-to-posix.mjs";
/**
* @typedef {(pString:string, pIndex: number, pArray: string[]) => boolean} FilterFunctionType
*/
/**
* @typedef {{directoryName: string; ignoreMatcher: import("ignore").Ignore}} IgnoreRuleType
*/
/**
* @param {string} pFullPathToFile
* @param {string} pBaseDirectory
* @returns {boolean}
*/
function fileIsDirectory(pFullPathToFile, pBaseDirectory) {
const lStat = statSync(join(pBaseDirectory, pFullPathToFile), {
throwIfNoEntry: false,
});
return lStat?.isDirectory() ?? false;
}
/**
* @param {string} pFileName
* @returns {string}
*/
function readIgnoreFile(pFileName) {
try {
return readFileSync(pFileName, "utf8");
} catch {
return "";
}
}
/**
* @param {string} pDirectoryName
* @param {string} pBaseDirectory
* @returns {string}
*/
function normalizeDirectoryName(pDirectoryName, pBaseDirectory) {
return pathToPosix(
relative(pBaseDirectory, join(pBaseDirectory, pDirectoryName)),
);
}
/**
* @param {string} pDirectoryName
* @param {string} pIgnoreFileContents
* @param {string[]=} pAdditionalIgnorePatterns
* @returns {IgnoreRuleType}
*/
function createIgnoreRule(
pDirectoryName,
pIgnoreFileContents,
pAdditionalIgnorePatterns = [],
) {
return {
directoryName: pDirectoryName,
ignoreMatcher: ignore()
.add(pIgnoreFileContents)
.add(pAdditionalIgnorePatterns),
};
}
/**
* @param {string} pDirectoryName
* @param {string} pBaseDirectory
* @param {{ignoreFileContents?: string}} pOptions
* @returns {IgnoreRuleType}
*/
function createIgnoreRuleForDirectory(
pDirectoryName,
pBaseDirectory,
pOptions,
) {
const lIgnoreFileContents =
pOptions.ignoreFileContents === undefined
? readIgnoreFile(join(pBaseDirectory, pDirectoryName, ".gitignore"))
: pOptions.ignoreFileContents;
return createIgnoreRule(pDirectoryName, lIgnoreFileContents);
}
/**
* @param {string[]} pAncestorDirectoryNames
* @param {string} pBaseDirectory
* @returns {IgnoreRuleType[]}
*/
function createIgnoreRulesFromDirectoryNames(
pAncestorDirectoryNames,
pBaseDirectory,
) {
return pAncestorDirectoryNames.map((pAncestorDirectoryName) =>
createIgnoreRuleForDirectory(pAncestorDirectoryName, pBaseDirectory, {}),
);
}
/**
* @param {string} pDirectoryName
* @param {string} pBaseDirectory
* @returns {IgnoreRuleType[]}
*/
function createIgnoreRulesBeforeDirectory(pDirectoryName, pBaseDirectory) {
const lNormalizedDirectoryName = normalizeDirectoryName(
pDirectoryName,
pBaseDirectory,
);
if (lNormalizedDirectoryName === "") {
return [];
}
const lDirectorySegments = lNormalizedDirectoryName.split("/");
const lAncestorDirectoryNames = [""];
let lCurrentDirectoryName = "";
for (const lDirectorySegment of lDirectorySegments.slice(0, -1)) {
lCurrentDirectoryName = lCurrentDirectoryName
? join(lCurrentDirectoryName, lDirectorySegment)
: lDirectorySegment;
lAncestorDirectoryNames.push(pathToPosix(lCurrentDirectoryName));
}
return createIgnoreRulesFromDirectoryNames(
lAncestorDirectoryNames,
pBaseDirectory,
);
}
/**
* @param {string} pFilePath
* @param {IgnoreRuleType[]} pIgnoreRules
* @param {IgnoreRuleType} pMandatoryIgnoreRule
* @returns {boolean}
*/
function fileShouldBeKept(pFilePath, pIgnoreRules, pMandatoryIgnoreRule) {
let lFileIsIgnored = false;
for (const lIgnoreRule of pIgnoreRules) {
const lDirectoryName = lIgnoreRule.directoryName;
const lIgnoreMatcher = lIgnoreRule.ignoreMatcher;
let lRelativePath = null;
if (lDirectoryName === "") {
lRelativePath = pFilePath;
} else if (pFilePath.startsWith(`${lDirectoryName}/`)) {
lRelativePath = pFilePath.slice(lDirectoryName.length + 1);
}
if (lRelativePath !== null) {
const { ignored: lIgnored, unignored: lUnignored } =
lIgnoreMatcher.test(lRelativePath);
if (lIgnored) {
lFileIsIgnored = true;
}
if (lUnignored) {
lFileIsIgnored = false;
}
}
}
const { ignored: lIgnored, unignored: lUnignored } =
pMandatoryIgnoreRule.ignoreMatcher.test(pFilePath);
if (lIgnored) {
lFileIsIgnored = true;
}
if (lUnignored) {
lFileIsIgnored = false;
}
return !lFileIsIgnored;
}
/**
* @type FilterFunctionType
*/
function identityFilter(_pString, _pIndex, _pArray) {
return true;
}
/**
* @param {string} pDirectoryName
* @param {{baseDir: string; ignoreRules: IgnoreRuleType[]; mandatoryIgnoreRule: IgnoreRuleType; startDirectoryName: string; startDirectoryIgnoreFileContents?: string; excludeFilterFn: FilterFunctionType; includeOnlyFilterFn: FilterFunctionType}}
* pOptions
* @returns {string[]}
*/
function walk(
pDirectoryName,
{
baseDir,
ignoreRules,
mandatoryIgnoreRule,
startDirectoryName,
startDirectoryIgnoreFileContents,
excludeFilterFn,
includeOnlyFilterFn,
},
) {
const lCurrentDirectoryName = normalizeDirectoryName(pDirectoryName, baseDir);
const lCurrentIgnoreRules = ignoreRules.concat(
createIgnoreRuleForDirectory(lCurrentDirectoryName, baseDir, {
...(lCurrentDirectoryName === startDirectoryName &&
startDirectoryIgnoreFileContents !== undefined
? { ignoreFileContents: startDirectoryIgnoreFileContents }
: {}),
}),
);
const lFilesInCurrentDirectory = readdirSync(join(baseDir, pDirectoryName))
.map((pFileName) => join(pDirectoryName, pFileName))
.filter((pFilePath) =>
fileShouldBeKept(pFilePath, lCurrentIgnoreRules, mandatoryIgnoreRule),
)
.filter(excludeFilterFn)
.filter(includeOnlyFilterFn);
const lFiles = [];
for (const lFile of lFilesInCurrentDirectory) {
if (fileIsDirectory(lFile, baseDir)) {
lFiles.push(
...walk(lFile, {
baseDir,
ignoreRules: lCurrentIgnoreRules,
mandatoryIgnoreRule,
startDirectoryName,
startDirectoryIgnoreFileContents,
excludeFilterFn,
includeOnlyFilterFn,
}),
);
} else {
lFiles.push(pathToPosix(lFile));
}
}
return lFiles;
}
/**
* @param {string} pDirectoryName
* @param {{baseDir: string; ignoreFileContents?: string; additionalIgnorePatterns?: string[]; excludeFilterFn?: FilterFunctionType; includeOnlyFilterFn?: FilterFunctionType}}
* pOptions
* @returns {string[]}
*/
export default function findAllFiles(
pDirectoryName,
{
baseDir,
ignoreFileContents,
additionalIgnorePatterns,
excludeFilterFn,
includeOnlyFilterFn,
},
) {
const lAdditionalIgnorePatterns = additionalIgnorePatterns ?? [".git"];
const lStartDirectoryName = normalizeDirectoryName(pDirectoryName, baseDir);
const lMandatoryIgnoreRule = createIgnoreRule(
"",
"",
lAdditionalIgnorePatterns,
);
const lIgnoreRules = createIgnoreRulesBeforeDirectory(
pDirectoryName,
baseDir,
);
return walk(pDirectoryName, {
baseDir,
ignoreRules: lIgnoreRules,
mandatoryIgnoreRule: lMandatoryIgnoreRule,
startDirectoryName: lStartDirectoryName,
startDirectoryIgnoreFileContents: ignoreFileContents,
excludeFilterFn: excludeFilterFn ?? identityFilter,
includeOnlyFilterFn: includeOnlyFilterFn ?? identityFilter,
});
}