UNPKG

dependency-cruiser

Version:

Validate and visualize dependencies. With your rules. JavaScript, TypeScript, CoffeeScript. ES6, CommonJS, AMD.

248 lines (219 loc) 6.98 kB
/* eslint-disable security/detect-object-injection */ import { normalizeREProperties } from "../helpers.mjs"; import defaults from "./defaults.mjs"; import { uniq } from "#utl/array-util.mjs"; /** * @import { ICruiseResult } from "../../../types/cruise-result.mjs"; * @import { ICruiseOptions, IFormatOptions } from "../../../types/options.mjs"; * @import { IStrictCruiseOptions, IStrictFormatOptions } from "../../../types/strict-options.mjs"; * @import { IForbiddenRuleType, IFlattenedRuleSet } from "../../../types/rule-set.mjs"; * @import { ICacheOptions } from "../../../types/cache-options.mjs"; */ const DEFAULT_CACHE_FOLDER = "node_modules/.cache/dependency-cruiser"; const DEFAULT_CACHE_STRATEGY = "metadata"; function normalizeFilterOption(pFilterOption) { let lReturnValue = pFilterOption || {}; if (typeof lReturnValue === "string" || Array.isArray(lReturnValue)) { lReturnValue = { path: lReturnValue, }; } return normalizeREProperties(lReturnValue, ["path"]); } function normalizeReporterOptions(pReporterOptions) { const lNormalizeableOptions = [ "archi.collapsePattern", "archi.filters.includeOnly.path", "archi.filters.focus.path", "archi.filters.exclude.path", "dot.collapsePattern", "dot.filters.includeOnly.path", "dot.filters.focus.path", "dot.filters.exclude.path", "ddot.collapsePattern", "ddot.filters.includeOnly.path", "ddot.filters.focus.path", "ddot.filters.exclude.path", ]; return normalizeREProperties(pReporterOptions, lNormalizeableOptions); } function normalizeFilterOptions(pOptions, pFilterOptionKeys) { let lReturnValue = { ...pOptions }; for (const lFilterOptionKey of pFilterOptionKeys) { if (pOptions[lFilterOptionKey]) { lReturnValue[lFilterOptionKey] = normalizeFilterOption( lReturnValue[lFilterOptionKey], ); } } return lReturnValue; } function normalizeCollapse(pCollapse) { let lReturnValue = pCollapse; const lOneOrMoreNonSlashes = "[^/]+"; const lFolderPattern = `${lOneOrMoreNonSlashes}/`; const lFolderBelowNodeModules = `node_modules/${lOneOrMoreNonSlashes}`; const lSingleDigitRe = /^\d$/; if (typeof pCollapse === "number" || lSingleDigitRe.test(pCollapse)) { lReturnValue = `${lFolderBelowNodeModules}|^${lFolderPattern.repeat( Number.parseInt(pCollapse, 10), )}`; } return lReturnValue; } function normalizeFocusDepth(pFormatOptions) { /** @type {IFormatOptions}*/ let lFormatOptions = structuredClone(pFormatOptions); if (Object.hasOwn(lFormatOptions, "focusDepth")) { if (lFormatOptions?.focus) { lFormatOptions.focus.depth = Number.parseInt( lFormatOptions.focusDepth, 10, ); } delete lFormatOptions.focusDepth; } return lFormatOptions; } /** * * @param {IForbiddenRuleType} pRule * @returns {boolean} */ function hasMetricsRule(pRule) { // TODO: philosophy: is a rule with 'folder' in it a metrics rule? // Or is it a misuse to ensure folder derivations (like cycles) get // kicked off? return ( Object.hasOwn(pRule?.to ?? {}, "moreUnstable") || (pRule?.scope ?? "module") === "folder" ); } /** * * @param {IFlattenedRuleSet} pRuleSet * @returns {boolean} */ function ruleSetHasMetricsRule(pRuleSet) { const lRuleSet = pRuleSet || {}; return ( (lRuleSet.forbidden || []).some(hasMetricsRule) || (lRuleSet.allowed || []).some(hasMetricsRule) ); } /** * * @param {ICruiseOptions} pOptions * @returns Boolean */ function reporterShowsMetrics(pOptions) { return ( (pOptions.reporterOptions?.[pOptions?.outputType]?.showMetrics ?? false) === true ); } /** * Determines whether (instability) metrics should be calculated * * @param {ICruiseOptions} pOptions * @returns Boolean */ function shouldCalculateMetrics(pOptions) { return ( pOptions.metrics || pOptions.outputType === "metrics" || reporterShowsMetrics(pOptions) || ruleSetHasMetricsRule(pOptions.ruleSet) ); } /** * @param {string|boolean|Partial<ICacheOptions>} pCacheOptions * @returns {ICacheOptions} */ function normalizeCacheOptions(pCacheOptions) { let lNormalizedCacheOptions = pCacheOptions; if (typeof pCacheOptions === "string") { lNormalizedCacheOptions = { folder: pCacheOptions, }; } if (pCacheOptions === true) { lNormalizedCacheOptions = {}; } // TODO: put these values in a central spot as constants // (folder is already in src/cli/defaults.js but depending on the UI (cli) // is not OK. We might b.t.w. wanna deduplicate the defaulting) return { folder: DEFAULT_CACHE_FOLDER, strategy: DEFAULT_CACHE_STRATEGY, ...lNormalizedCacheOptions, }; } /** * * @param {ICruiseOptions} pOptions * @param {string[]} pFileAndDirectoryArray * @returns {IStrictCruiseOptions} */ export function normalizeCruiseOptions(pOptions, pFileAndDirectoryArray = []) { /** @type {IStrictCruiseOptions} */ let lReturnValue = { baseDir: process.cwd(), ...defaults, ...pOptions, args: pFileAndDirectoryArray.join(" "), }; // @ts-expect-error the idea of normalizing maxDepth to number is that after // that we're sure it's a number. Should maybe best be solved by // having two types/ interfaces lReturnValue.maxDepth = Number.parseInt(lReturnValue.maxDepth, 10); lReturnValue.moduleSystems = uniq(lReturnValue.moduleSystems); if (Object.hasOwn(lReturnValue, "collapse")) { lReturnValue.collapse = normalizeCollapse(lReturnValue.collapse); } // TODO: further down the execution path code still relies on .doNotFollow // and .exclude existing. We should treat them the same as the // other filters (so either make all exist always or only // when they're actually defined) lReturnValue.doNotFollow = normalizeFilterOption(lReturnValue.doNotFollow); lReturnValue.exclude = normalizeFilterOption(lReturnValue.exclude); lReturnValue.extraExtensionsToScan = lReturnValue.extraExtensionsToScan || []; lReturnValue = normalizeFilterOptions(lReturnValue, [ "focus", "includeOnly", "reaches", "highlight", ]); lReturnValue.exoticRequireStrings = uniq(lReturnValue.exoticRequireStrings); if (lReturnValue.reporterOptions) { lReturnValue.reporterOptions = normalizeReporterOptions( lReturnValue.reporterOptions, ); } lReturnValue.metrics = shouldCalculateMetrics(pOptions); if (lReturnValue.cache) { lReturnValue.cache = normalizeCacheOptions(lReturnValue.cache); } if (lReturnValue.detectJSDocImports) { lReturnValue.parser = "tsc"; } return normalizeFocusDepth(lReturnValue); } /** * @param {IFormatOptions} pFormatOptions * @returns {IStrictFormatOptions} */ export function normalizeFormatOptions(pFormatOptions) { let lFormatOptions = structuredClone(pFormatOptions); if (Object.hasOwn(lFormatOptions, "collapse")) { lFormatOptions.collapse = normalizeCollapse(lFormatOptions.collapse); } lFormatOptions = normalizeFilterOptions(lFormatOptions, [ "exclude", "focus", "highlight", "includeOnly", "reaches", ]); return normalizeFocusDepth(lFormatOptions); }