dep-brain 0.2.1 → 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
@@ -91,6 +92,10 @@ Suggestions:
91
92
  dep-brain analyze --json
92
93
  ```
93
94
 
95
+ Output includes `outputVersion` for schema stability and can be validated with:
96
+
97
+ - `depbrain.output.schema.json`
98
+
94
99
  ## Markdown Output
95
100
 
96
101
  ```bash
@@ -105,7 +110,9 @@ Create a `depbrain.config.json` file in the project root:
105
110
  {
106
111
  "ignore": {
107
112
  "unused": ["eslint"],
108
- "outdated": ["typescript"]
113
+ "outdated": ["typescript"],
114
+ "prefixes": ["@nestjs/"],
115
+ "patterns": ["^@aws-sdk/"]
109
116
  },
110
117
  "policy": {
111
118
  "minScore": 90,
@@ -120,6 +127,9 @@ Create a `depbrain.config.json` file in the project root:
120
127
  "outdatedWeight": 1,
121
128
  "unusedWeight": 2,
122
129
  "riskWeight": 6
130
+ },
131
+ "scan": {
132
+ "excludePaths": ["node_modules", "dist", "build", "coverage", ".git"]
123
133
  }
124
134
  }
125
135
  ```
@@ -142,11 +152,15 @@ Supported sections:
142
152
  - `scoring.outdatedWeight`
143
153
  - `scoring.unusedWeight`
144
154
  - `scoring.riskWeight`
155
+ - `ignore.prefixes`
156
+ - `ignore.patterns`
157
+ - `scan.excludePaths`
145
158
 
146
159
  Sample config file:
147
160
 
148
161
  - `depbrain.config.json`
149
162
  - `depbrain.config.schema.json`
163
+ - `depbrain.output.schema.json`
150
164
 
151
165
  ## CI Behavior
152
166
 
@@ -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
  }
@@ -0,0 +1,112 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "Dependency Brain Analysis Output",
4
+ "type": "object",
5
+ "required": ["outputVersion", "rootDir", "score", "scoreBreakdown", "policy", "duplicates", "unused", "outdated", "risks", "suggestions", "config"],
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "outputVersion": { "type": "string" },
9
+ "rootDir": { "type": "string" },
10
+ "score": { "type": "number" },
11
+ "scoreBreakdown": {
12
+ "type": "object",
13
+ "required": ["baseScore", "duplicates", "outdated", "unused", "risks", "weights"],
14
+ "additionalProperties": false,
15
+ "properties": {
16
+ "baseScore": { "type": "number" },
17
+ "duplicates": { "type": "number" },
18
+ "outdated": { "type": "number" },
19
+ "unused": { "type": "number" },
20
+ "risks": { "type": "number" },
21
+ "weights": {
22
+ "type": "object",
23
+ "required": ["duplicateWeight", "outdatedWeight", "unusedWeight", "riskWeight"],
24
+ "additionalProperties": false,
25
+ "properties": {
26
+ "duplicateWeight": { "type": "number" },
27
+ "outdatedWeight": { "type": "number" },
28
+ "unusedWeight": { "type": "number" },
29
+ "riskWeight": { "type": "number" }
30
+ }
31
+ }
32
+ }
33
+ },
34
+ "policy": {
35
+ "type": "object",
36
+ "required": ["passed", "reasons"],
37
+ "additionalProperties": false,
38
+ "properties": {
39
+ "passed": { "type": "boolean" },
40
+ "reasons": { "type": "array", "items": { "type": "string" } }
41
+ }
42
+ },
43
+ "duplicates": {
44
+ "type": "array",
45
+ "items": {
46
+ "type": "object",
47
+ "required": ["name", "versions", "instances"],
48
+ "additionalProperties": false,
49
+ "properties": {
50
+ "name": { "type": "string" },
51
+ "versions": { "type": "array", "items": { "type": "string" } },
52
+ "instances": {
53
+ "type": "array",
54
+ "items": {
55
+ "type": "object",
56
+ "required": ["path", "version"],
57
+ "additionalProperties": false,
58
+ "properties": {
59
+ "path": { "type": "string" },
60
+ "version": { "type": "string" }
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ },
67
+ "unused": {
68
+ "type": "array",
69
+ "items": {
70
+ "type": "object",
71
+ "required": ["name", "section"],
72
+ "additionalProperties": false,
73
+ "properties": {
74
+ "name": { "type": "string" },
75
+ "section": { "type": "string", "enum": ["dependencies", "devDependencies"] },
76
+ "package": { "type": "string" }
77
+ }
78
+ }
79
+ },
80
+ "outdated": {
81
+ "type": "array",
82
+ "items": {
83
+ "type": "object",
84
+ "required": ["name", "current", "latest", "updateType"],
85
+ "additionalProperties": false,
86
+ "properties": {
87
+ "name": { "type": "string" },
88
+ "current": { "type": "string" },
89
+ "latest": { "type": "string" },
90
+ "updateType": { "type": "string" },
91
+ "package": { "type": "string" }
92
+ }
93
+ }
94
+ },
95
+ "risks": {
96
+ "type": "array",
97
+ "items": {
98
+ "type": "object",
99
+ "required": ["name", "reasons"],
100
+ "additionalProperties": false,
101
+ "properties": {
102
+ "name": { "type": "string" },
103
+ "reasons": { "type": "array", "items": { "type": "string" } },
104
+ "package": { "type": "string" }
105
+ }
106
+ }
107
+ },
108
+ "suggestions": { "type": "array", "items": { "type": "string" } },
109
+ "config": { "type": "object" },
110
+ "packages": { "type": "array" }
111
+ }
112
+ }
@@ -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();
@@ -31,6 +31,7 @@ export interface RiskDependency {
31
31
  package?: string;
32
32
  }
33
33
  export interface AnalysisResult {
34
+ outputVersion: string;
34
35
  rootDir: string;
35
36
  score: number;
36
37
  scoreBreakdown: ScoreBreakdown;
@@ -59,6 +60,7 @@ export interface PackageAnalysisResult {
59
60
  risks: RiskDependency[];
60
61
  suggestions: string[];
61
62
  }
63
+ export declare const OUTPUT_VERSION = "1.0";
62
64
  export interface ScoreBreakdown {
63
65
  baseScore: number;
64
66
  duplicates: number;
@@ -7,6 +7,7 @@ import { loadDepBrainConfig } from "../utils/config.js";
7
7
  import { findWorkspacePackages } from "../utils/workspaces.js";
8
8
  import { buildDependencyGraph } from "./graph-builder.js";
9
9
  import { calculateHealthScore } from "./scorer.js";
10
+ export const OUTPUT_VERSION = "1.0";
10
11
  export async function analyzeProject(options = {}) {
11
12
  const rootDir = path.resolve(options.rootDir ?? process.cwd());
12
13
  const loadedConfig = await loadDepBrainConfig(rootDir, options.configPath);
@@ -17,7 +18,7 @@ export async function analyzeProject(options = {}) {
17
18
  }
18
19
  const rootGraph = await buildDependencyGraph(rootDir);
19
20
  const rawDuplicates = await findDuplicateDependencies(rootGraph);
20
- const duplicates = rawDuplicates.filter((item) => !config.ignore.duplicates.includes(item.name));
21
+ const duplicates = rawDuplicates.filter((item) => !shouldIgnorePackage(item.name, "duplicates", config));
21
22
  const packages = [];
22
23
  for (const workspace of workspaces) {
23
24
  const result = await analyzeSingleProject(workspace.rootDir, config, {
@@ -55,6 +56,7 @@ export async function analyzeProject(options = {}) {
55
56
  risks: risks.length
56
57
  }, config);
57
58
  return {
59
+ outputVersion: OUTPUT_VERSION,
58
60
  rootDir,
59
61
  score,
60
62
  scoreBreakdown,
@@ -79,7 +81,9 @@ function mergeConfig(base, overrides) {
79
81
  duplicates: overrides.ignore?.duplicates ?? base.ignore.duplicates,
80
82
  outdated: overrides.ignore?.outdated ?? base.ignore.outdated,
81
83
  risks: overrides.ignore?.risks ?? base.ignore.risks,
82
- 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
83
87
  },
84
88
  policy: {
85
89
  minScore: overrides.policy?.minScore ?? base.policy.minScore,
@@ -96,6 +100,9 @@ function mergeConfig(base, overrides) {
96
100
  outdatedWeight: overrides.scoring?.outdatedWeight ?? base.scoring.outdatedWeight,
97
101
  unusedWeight: overrides.scoring?.unusedWeight ?? base.scoring.unusedWeight,
98
102
  riskWeight: overrides.scoring?.riskWeight ?? base.scoring.riskWeight
103
+ },
104
+ scan: {
105
+ excludePaths: overrides.scan?.excludePaths ?? base.scan.excludePaths
99
106
  }
100
107
  };
101
108
  }
@@ -125,15 +132,17 @@ async function analyzeSingleProject(rootDir, config, options = {}) {
125
132
  const graph = await buildDependencyGraph(rootDir);
126
133
  const [rawDuplicates, rawUnused, rawOutdated, rawRisks] = await Promise.all([
127
134
  findDuplicateDependencies(graph),
128
- findUnusedDependencies(rootDir, graph),
135
+ findUnusedDependencies(rootDir, graph, {
136
+ excludePaths: config.scan.excludePaths
137
+ }),
129
138
  findOutdatedDependencies(graph),
130
139
  findRiskDependencies(graph)
131
140
  ]);
132
- const duplicates = rawDuplicates.filter((item) => !config.ignore.duplicates.includes(item.name));
133
- const unused = rawUnused.filter((item) => !config.ignore.unused.includes(item.name) &&
134
- !config.ignore[item.section].includes(item.name));
135
- const outdated = rawOutdated.filter((item) => !config.ignore.outdated.includes(item.name));
136
- 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));
137
146
  const score = calculateHealthScore({
138
147
  duplicates: duplicates.length,
139
148
  unused: unused.length,
@@ -172,6 +181,7 @@ async function analyzeSingleProject(rootDir, config, options = {}) {
172
181
  ? risks.map((item) => ({ ...item, package: options.packageName }))
173
182
  : risks;
174
183
  return {
184
+ outputVersion: OUTPUT_VERSION,
175
185
  rootDir,
176
186
  score,
177
187
  scoreBreakdown,
@@ -184,6 +194,23 @@ async function analyzeSingleProject(rootDir, config, options = {}) {
184
194
  config
185
195
  };
186
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
+ }
187
214
  function buildScoreBreakdown(counts, config) {
188
215
  return {
189
216
  baseScore: 100,
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { analyzeProject } from "./core/analyzer.js";
2
2
  export type { AnalysisOptions, AnalysisResult, DuplicateDependency, OutdatedDependency, PolicyResult, PackageAnalysisResult, ScoreBreakdown, RiskDependency, UnusedDependency } from "./core/analyzer.js";
3
+ export { OUTPUT_VERSION } from "./core/analyzer.js";
3
4
  export type { DepBrainConfig, DepBrainConfigOverrides } from "./utils/config.js";
4
5
  export type { WorkspacePackage } from "./utils/workspaces.js";
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export { analyzeProject } from "./core/analyzer.js";
2
+ export { OUTPUT_VERSION } from "./core/analyzer.js";
@@ -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.2.1",
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",
@@ -14,7 +14,8 @@
14
14
  "LICENSE",
15
15
  "CHANGELOG.md",
16
16
  "depbrain.config.json",
17
- "depbrain.config.schema.json"
17
+ "depbrain.config.schema.json",
18
+ "depbrain.output.schema.json"
18
19
  ],
19
20
  "scripts": {
20
21
  "build": "tsc -p tsconfig.json",