dep-brain 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,6 +46,7 @@ npx dep-brain analyze ./path-to-project
46
46
  npx dep-brain analyze --config depbrain.config.json
47
47
  npx dep-brain analyze --min-score 90 --fail-on-risks
48
48
  npx dep-brain analyze ./path-to-project --fail-on-unused --json
49
+ npx dep-brain analyze --md > depbrain.md
49
50
 
50
51
  dep-brain config
51
52
  dep-brain config --config depbrain.config.json
@@ -109,7 +110,9 @@ Create a `depbrain.config.json` file in the project root:
109
110
  {
110
111
  "ignore": {
111
112
  "unused": ["eslint"],
112
- "outdated": ["typescript"]
113
+ "outdated": ["typescript"],
114
+ "prefixes": ["@nestjs/"],
115
+ "patterns": ["^@aws-sdk/"]
113
116
  },
114
117
  "policy": {
115
118
  "minScore": 90,
@@ -124,6 +127,9 @@ Create a `depbrain.config.json` file in the project root:
124
127
  "outdatedWeight": 1,
125
128
  "unusedWeight": 2,
126
129
  "riskWeight": 6
130
+ },
131
+ "scan": {
132
+ "excludePaths": ["node_modules", "dist", "build", "coverage", ".git"]
127
133
  }
128
134
  }
129
135
  ```
@@ -146,6 +152,9 @@ Supported sections:
146
152
  - `scoring.outdatedWeight`
147
153
  - `scoring.unusedWeight`
148
154
  - `scoring.riskWeight`
155
+ - `ignore.prefixes`
156
+ - `ignore.patterns`
157
+ - `scan.excludePaths`
149
158
 
150
159
  Sample config file:
151
160
 
@@ -5,7 +5,9 @@
5
5
  "duplicates": [],
6
6
  "risks": [],
7
7
  "dependencies": [],
8
- "devDependencies": []
8
+ "devDependencies": [],
9
+ "prefixes": [],
10
+ "patterns": []
9
11
  },
10
12
  "policy": {
11
13
  "minScore": 85,
@@ -22,5 +24,8 @@
22
24
  "outdatedWeight": 3,
23
25
  "unusedWeight": 4,
24
26
  "riskWeight": 10
27
+ },
28
+ "scan": {
29
+ "excludePaths": ["node_modules", "dist", "build", "coverage", ".git"]
25
30
  }
26
31
  }
@@ -13,7 +13,9 @@
13
13
  "unused": { "type": "array", "items": { "type": "string" } },
14
14
  "duplicates": { "type": "array", "items": { "type": "string" } },
15
15
  "outdated": { "type": "array", "items": { "type": "string" } },
16
- "risks": { "type": "array", "items": { "type": "string" } }
16
+ "risks": { "type": "array", "items": { "type": "string" } },
17
+ "prefixes": { "type": "array", "items": { "type": "string" } },
18
+ "patterns": { "type": "array", "items": { "type": "string" } }
17
19
  }
18
20
  },
19
21
  "policy": {
@@ -43,6 +45,13 @@
43
45
  "unusedWeight": { "type": "number" },
44
46
  "riskWeight": { "type": "number" }
45
47
  }
48
+ },
49
+ "scan": {
50
+ "type": "object",
51
+ "additionalProperties": false,
52
+ "properties": {
53
+ "excludePaths": { "type": "array", "items": { "type": "string" } }
54
+ }
46
55
  }
47
56
  }
48
57
  }
@@ -1,3 +1,5 @@
1
1
  import type { UnusedDependency } from "../core/analyzer.js";
2
2
  import type { DependencyGraph } from "../core/graph-builder.js";
3
- export declare function findUnusedDependencies(rootDir: string, graph: DependencyGraph): Promise<UnusedDependency[]>;
3
+ export declare function findUnusedDependencies(rootDir: string, graph: DependencyGraph, options?: {
4
+ excludePaths?: string[];
5
+ }): Promise<UnusedDependency[]>;
@@ -4,8 +4,8 @@ const SOURCE_FILE_PATTERN = /\.(c|m)?(t|j)sx?$/;
4
4
  const CONFIG_FILE_PATTERN = /(^|[\\/])(vite|vitest|jest|eslint|prettier|rollup|webpack|babel|tsup|eslint\.config|commitlint|playwright|storybook|tailwind|postcss)\.config\.(c|m)?(t|j)s$/;
5
5
  const TEST_FILE_PATTERN = /(^|[\\/])(__tests__|test|tests|spec|specs)([\\/]|$)|\.(test|spec)\.(c|m)?(t|j)sx?$/;
6
6
  const RUNTIME_DIR_PATTERN = /(^|[\\/])(src|app|lib|server|client|pages|components)([\\/]|$)/;
7
- export async function findUnusedDependencies(rootDir, graph) {
8
- const files = await collectProjectFiles(rootDir, SOURCE_FILE_PATTERN);
7
+ export async function findUnusedDependencies(rootDir, graph, options = {}) {
8
+ const files = await collectProjectFiles(rootDir, SOURCE_FILE_PATTERN, options.excludePaths ?? []);
9
9
  const projectFiles = files.filter((filePath) => !filePath.includes(`${path.sep}node_modules${path.sep}`));
10
10
  const runtimeUsed = new Set();
11
11
  const devUsed = new Set();
@@ -18,7 +18,7 @@ export async function analyzeProject(options = {}) {
18
18
  }
19
19
  const rootGraph = await buildDependencyGraph(rootDir);
20
20
  const rawDuplicates = await findDuplicateDependencies(rootGraph);
21
- const duplicates = rawDuplicates.filter((item) => !config.ignore.duplicates.includes(item.name));
21
+ const duplicates = rawDuplicates.filter((item) => !shouldIgnorePackage(item.name, "duplicates", config));
22
22
  const packages = [];
23
23
  for (const workspace of workspaces) {
24
24
  const result = await analyzeSingleProject(workspace.rootDir, config, {
@@ -81,7 +81,9 @@ function mergeConfig(base, overrides) {
81
81
  duplicates: overrides.ignore?.duplicates ?? base.ignore.duplicates,
82
82
  outdated: overrides.ignore?.outdated ?? base.ignore.outdated,
83
83
  risks: overrides.ignore?.risks ?? base.ignore.risks,
84
- unused: overrides.ignore?.unused ?? base.ignore.unused
84
+ unused: overrides.ignore?.unused ?? base.ignore.unused,
85
+ prefixes: overrides.ignore?.prefixes ?? base.ignore.prefixes,
86
+ patterns: overrides.ignore?.patterns ?? base.ignore.patterns
85
87
  },
86
88
  policy: {
87
89
  minScore: overrides.policy?.minScore ?? base.policy.minScore,
@@ -98,6 +100,9 @@ function mergeConfig(base, overrides) {
98
100
  outdatedWeight: overrides.scoring?.outdatedWeight ?? base.scoring.outdatedWeight,
99
101
  unusedWeight: overrides.scoring?.unusedWeight ?? base.scoring.unusedWeight,
100
102
  riskWeight: overrides.scoring?.riskWeight ?? base.scoring.riskWeight
103
+ },
104
+ scan: {
105
+ excludePaths: overrides.scan?.excludePaths ?? base.scan.excludePaths
101
106
  }
102
107
  };
103
108
  }
@@ -127,15 +132,17 @@ async function analyzeSingleProject(rootDir, config, options = {}) {
127
132
  const graph = await buildDependencyGraph(rootDir);
128
133
  const [rawDuplicates, rawUnused, rawOutdated, rawRisks] = await Promise.all([
129
134
  findDuplicateDependencies(graph),
130
- findUnusedDependencies(rootDir, graph),
135
+ findUnusedDependencies(rootDir, graph, {
136
+ excludePaths: config.scan.excludePaths
137
+ }),
131
138
  findOutdatedDependencies(graph),
132
139
  findRiskDependencies(graph)
133
140
  ]);
134
- const duplicates = rawDuplicates.filter((item) => !config.ignore.duplicates.includes(item.name));
135
- const unused = rawUnused.filter((item) => !config.ignore.unused.includes(item.name) &&
136
- !config.ignore[item.section].includes(item.name));
137
- const outdated = rawOutdated.filter((item) => !config.ignore.outdated.includes(item.name));
138
- const risks = rawRisks.filter((item) => !config.ignore.risks.includes(item.name));
141
+ const duplicates = rawDuplicates.filter((item) => !shouldIgnorePackage(item.name, "duplicates", config));
142
+ const unused = rawUnused.filter((item) => !shouldIgnorePackage(item.name, "unused", config) &&
143
+ !shouldIgnorePackage(item.name, item.section, config));
144
+ const outdated = rawOutdated.filter((item) => !shouldIgnorePackage(item.name, "outdated", config));
145
+ const risks = rawRisks.filter((item) => !shouldIgnorePackage(item.name, "risks", config));
139
146
  const score = calculateHealthScore({
140
147
  duplicates: duplicates.length,
141
148
  unused: unused.length,
@@ -187,6 +194,23 @@ async function analyzeSingleProject(rootDir, config, options = {}) {
187
194
  config
188
195
  };
189
196
  }
197
+ function shouldIgnorePackage(name, bucket, config) {
198
+ if (config.ignore[bucket].includes(name)) {
199
+ return true;
200
+ }
201
+ if (config.ignore.prefixes.some((prefix) => name.startsWith(prefix))) {
202
+ return true;
203
+ }
204
+ return config.ignore.patterns.some((pattern) => {
205
+ try {
206
+ const regex = new RegExp(pattern);
207
+ return regex.test(name);
208
+ }
209
+ catch {
210
+ return false;
211
+ }
212
+ });
213
+ }
190
214
  function buildScoreBreakdown(counts, config) {
191
215
  return {
192
216
  baseScore: 100,
@@ -6,6 +6,8 @@ export interface DepBrainConfig {
6
6
  outdated: string[];
7
7
  risks: string[];
8
8
  unused: string[];
9
+ prefixes: string[];
10
+ patterns: string[];
9
11
  };
10
12
  policy: {
11
13
  minScore: number;
@@ -23,12 +25,16 @@ export interface DepBrainConfig {
23
25
  unusedWeight: number;
24
26
  riskWeight: number;
25
27
  };
28
+ scan: {
29
+ excludePaths: string[];
30
+ };
26
31
  }
27
32
  export interface DepBrainConfigOverrides {
28
33
  ignore?: Partial<DepBrainConfig["ignore"]>;
29
34
  policy?: Partial<DepBrainConfig["policy"]>;
30
35
  report?: Partial<DepBrainConfig["report"]>;
31
36
  scoring?: Partial<DepBrainConfig["scoring"]>;
37
+ scan?: Partial<DepBrainConfig["scan"]>;
32
38
  }
33
39
  export declare const defaultConfig: DepBrainConfig;
34
40
  export declare function loadDepBrainConfig(rootDir: string, configPath?: string): Promise<DepBrainConfig>;
@@ -7,7 +7,9 @@ export const defaultConfig = {
7
7
  duplicates: [],
8
8
  outdated: [],
9
9
  risks: [],
10
- unused: []
10
+ unused: [],
11
+ prefixes: [],
12
+ patterns: []
11
13
  },
12
14
  policy: {
13
15
  minScore: 0,
@@ -24,6 +26,9 @@ export const defaultConfig = {
24
26
  outdatedWeight: 3,
25
27
  unusedWeight: 4,
26
28
  riskWeight: 10
29
+ },
30
+ scan: {
31
+ excludePaths: ["node_modules", "dist", "build", "coverage", ".git"]
27
32
  }
28
33
  };
29
34
  export async function loadDepBrainConfig(rootDir, configPath) {
@@ -44,7 +49,9 @@ function normalizeConfig(loaded) {
44
49
  duplicates: normalizeStringArray(loaded.ignore?.duplicates, defaultConfig.ignore.duplicates),
45
50
  outdated: normalizeStringArray(loaded.ignore?.outdated, defaultConfig.ignore.outdated),
46
51
  risks: normalizeStringArray(loaded.ignore?.risks, defaultConfig.ignore.risks),
47
- unused: normalizeStringArray(loaded.ignore?.unused, defaultConfig.ignore.unused)
52
+ unused: normalizeStringArray(loaded.ignore?.unused, defaultConfig.ignore.unused),
53
+ prefixes: normalizeStringArray(loaded.ignore?.prefixes, defaultConfig.ignore.prefixes),
54
+ patterns: normalizeStringArray(loaded.ignore?.patterns, defaultConfig.ignore.patterns)
48
55
  },
49
56
  policy: {
50
57
  minScore: normalizeNumber(loaded.policy?.minScore, defaultConfig.policy.minScore),
@@ -61,6 +68,9 @@ function normalizeConfig(loaded) {
61
68
  outdatedWeight: normalizeNumber(loaded.scoring?.outdatedWeight, defaultConfig.scoring.outdatedWeight),
62
69
  unusedWeight: normalizeNumber(loaded.scoring?.unusedWeight, defaultConfig.scoring.unusedWeight),
63
70
  riskWeight: normalizeNumber(loaded.scoring?.riskWeight, defaultConfig.scoring.riskWeight)
71
+ },
72
+ scan: {
73
+ excludePaths: normalizeStringArray(loaded.scan?.excludePaths, defaultConfig.scan.excludePaths)
64
74
  }
65
75
  };
66
76
  }
@@ -1,3 +1,3 @@
1
1
  export declare function readJsonFile<T>(filePath: string): Promise<T>;
2
2
  export declare function readTextFile(filePath: string): Promise<string>;
3
- export declare function collectProjectFiles(rootDir: string, pattern: RegExp): Promise<string[]>;
3
+ export declare function collectProjectFiles(rootDir: string, pattern: RegExp, excludePaths?: string[]): Promise<string[]>;
@@ -7,16 +7,23 @@ export async function readJsonFile(filePath) {
7
7
  export async function readTextFile(filePath) {
8
8
  return fs.readFile(filePath, "utf8");
9
9
  }
10
- export async function collectProjectFiles(rootDir, pattern) {
11
- const entries = await fs.readdir(rootDir, { withFileTypes: true });
10
+ export async function collectProjectFiles(rootDir, pattern, excludePaths = []) {
11
+ return collectProjectFilesInternal(rootDir, rootDir, pattern, excludePaths);
12
+ }
13
+ async function collectProjectFilesInternal(currentDir, baseDir, pattern, excludePaths) {
14
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
12
15
  const files = [];
13
16
  for (const entry of entries) {
14
- const fullPath = path.join(rootDir, entry.name);
17
+ const fullPath = path.join(currentDir, entry.name);
18
+ const relPath = path.relative(baseDir, fullPath);
19
+ if (matchesAnyPattern(relPath, excludePaths)) {
20
+ continue;
21
+ }
15
22
  if (entry.isDirectory()) {
16
23
  if (entry.name === ".git") {
17
24
  continue;
18
25
  }
19
- files.push(...(await collectProjectFiles(fullPath, pattern)));
26
+ files.push(...(await collectProjectFilesInternal(fullPath, baseDir, pattern, excludePaths)));
20
27
  continue;
21
28
  }
22
29
  if (pattern.test(entry.name)) {
@@ -25,3 +32,26 @@ export async function collectProjectFiles(rootDir, pattern) {
25
32
  }
26
33
  return files;
27
34
  }
35
+ function matchesAnyPattern(value, patterns) {
36
+ if (patterns.length === 0) {
37
+ return false;
38
+ }
39
+ const normalized = normalizePath(value);
40
+ return patterns.some((pattern) => {
41
+ const regex = globToRegExp(pattern);
42
+ return regex.test(normalized);
43
+ });
44
+ }
45
+ function globToRegExp(pattern) {
46
+ const normalized = normalizePath(pattern)
47
+ .replace(/\/+$/, "")
48
+ .replace(/^\//, "")
49
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
50
+ .replace(/\*\*/g, "___DEPBRAIN_GLOBSTAR___")
51
+ .replace(/\*/g, "[^/]*")
52
+ .replace(/___DEPBRAIN_GLOBSTAR___/g, ".*");
53
+ return new RegExp(`(^|.*/)${normalized}($|/.*)`);
54
+ }
55
+ function normalizePath(value) {
56
+ return value.split(path.sep).join("/");
57
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dep-brain",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "CLI and library for dependency health analysis",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",