dep-brain 0.3.0 → 0.5.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,9 @@ 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
50
+ npx dep-brain analyze --json --out depbrain.json
51
+ npx dep-brain report --from depbrain.json --md --out depbrain.md
49
52
 
50
53
  dep-brain config
51
54
  dep-brain config --config depbrain.config.json
@@ -101,6 +104,13 @@ Output includes `outputVersion` for schema stability and can be validated with:
101
104
  dep-brain analyze --md
102
105
  ```
103
106
 
107
+ ## Report From JSON
108
+
109
+ ```bash
110
+ dep-brain analyze --json --out depbrain.json
111
+ dep-brain report --from depbrain.json --md --out depbrain.md
112
+ ```
113
+
104
114
  ## Config File
105
115
 
106
116
  Create a `depbrain.config.json` file in the project root:
@@ -109,7 +119,9 @@ Create a `depbrain.config.json` file in the project root:
109
119
  {
110
120
  "ignore": {
111
121
  "unused": ["eslint"],
112
- "outdated": ["typescript"]
122
+ "outdated": ["typescript"],
123
+ "prefixes": ["@nestjs/"],
124
+ "patterns": ["^@aws-sdk/"]
113
125
  },
114
126
  "policy": {
115
127
  "minScore": 90,
@@ -124,6 +136,9 @@ Create a `depbrain.config.json` file in the project root:
124
136
  "outdatedWeight": 1,
125
137
  "unusedWeight": 2,
126
138
  "riskWeight": 6
139
+ },
140
+ "scan": {
141
+ "excludePaths": ["node_modules", "dist", "build", "coverage", ".git"]
127
142
  }
128
143
  }
129
144
  ```
@@ -146,6 +161,9 @@ Supported sections:
146
161
  - `scoring.outdatedWeight`
147
162
  - `scoring.unusedWeight`
148
163
  - `scoring.riskWeight`
164
+ - `ignore.prefixes`
165
+ - `ignore.patterns`
166
+ - `scan.excludePaths`
149
167
 
150
168
  Sample config file:
151
169
 
@@ -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 { DuplicateDependency } from "../core/analyzer.js";
2
2
  import type { DependencyGraph } from "../core/graph-builder.js";
3
+ import type { CheckResult } from "../core/types.js";
3
4
  export declare function findDuplicateDependencies(graph: DependencyGraph): Promise<DuplicateDependency[]>;
5
+ export declare function runDuplicateCheck(graph: DependencyGraph): Promise<CheckResult>;
@@ -8,3 +8,20 @@ export async function findDuplicateDependencies(graph) {
8
8
  .filter((dependency) => dependency.versions.length > 1)
9
9
  .sort((left, right) => left.name.localeCompare(right.name));
10
10
  }
11
+ export async function runDuplicateCheck(graph) {
12
+ const duplicates = await findDuplicateDependencies(graph);
13
+ return {
14
+ name: "duplicate",
15
+ summary: `${duplicates.length} duplicate dependencies found`,
16
+ issues: duplicates.map((item) => ({
17
+ id: `duplicate:${item.name}`,
18
+ message: `${item.name} has ${item.versions.length} versions`,
19
+ severity: "warning",
20
+ meta: {
21
+ name: item.name,
22
+ versions: item.versions,
23
+ instances: item.instances
24
+ }
25
+ }))
26
+ };
27
+ }
@@ -1,6 +1,8 @@
1
1
  import type { OutdatedDependency } from "../core/analyzer.js";
2
2
  import type { DependencyGraph } from "../core/graph-builder.js";
3
+ import type { CheckResult } from "../core/types.js";
3
4
  export interface OutdatedOptions {
4
5
  resolveLatestVersion?: (name: string) => Promise<string | null>;
5
6
  }
6
7
  export declare function findOutdatedDependencies(graph: DependencyGraph, options?: OutdatedOptions): Promise<OutdatedDependency[]>;
8
+ export declare function runOutdatedCheck(graph: DependencyGraph): Promise<CheckResult>;
@@ -22,6 +22,24 @@ export async function findOutdatedDependencies(graph, options = {}) {
22
22
  .filter((item) => item !== null)
23
23
  .sort((left, right) => left.name.localeCompare(right.name));
24
24
  }
25
+ export async function runOutdatedCheck(graph) {
26
+ const outdated = await findOutdatedDependencies(graph);
27
+ return {
28
+ name: "outdated",
29
+ summary: `${outdated.length} outdated dependencies found`,
30
+ issues: outdated.map((item) => ({
31
+ id: `outdated:${item.name}`,
32
+ message: `${item.name} ${item.current} -> ${item.latest}`,
33
+ severity: item.updateType === "major" ? "critical" : "warning",
34
+ meta: {
35
+ name: item.name,
36
+ current: item.current,
37
+ latest: item.latest,
38
+ updateType: item.updateType
39
+ }
40
+ }))
41
+ };
42
+ }
25
43
  function normalizeVersion(versionRange) {
26
44
  return versionRange.trim().replace(/^[~^><=\s]+/, "");
27
45
  }
@@ -1,3 +1,5 @@
1
1
  import type { DependencyGraph } from "../core/graph-builder.js";
2
2
  import type { RiskDependency } from "../core/analyzer.js";
3
+ import type { CheckResult } from "../core/types.js";
3
4
  export declare function findRiskDependencies(graph: DependencyGraph): Promise<RiskDependency[]>;
5
+ export declare function runRiskCheck(graph: DependencyGraph): Promise<CheckResult>;
@@ -29,3 +29,19 @@ export async function findRiskDependencies(graph) {
29
29
  .filter((item) => item !== null)
30
30
  .sort((left, right) => left.name.localeCompare(right.name));
31
31
  }
32
+ export async function runRiskCheck(graph) {
33
+ const risks = await findRiskDependencies(graph);
34
+ return {
35
+ name: "risk",
36
+ summary: `${risks.length} risky dependencies found`,
37
+ issues: risks.map((item) => ({
38
+ id: `risk:${item.name}`,
39
+ message: `${item.name}: ${item.reasons.join("; ")}`,
40
+ severity: "warning",
41
+ meta: {
42
+ name: item.name,
43
+ reasons: item.reasons
44
+ }
45
+ }))
46
+ };
47
+ }
@@ -1,3 +1,10 @@
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
+ import type { AnalysisContext, CheckResult } from "../core/types.js";
4
+ export declare function findUnusedDependencies(rootDir: string, graph: DependencyGraph, fileEntries: {
5
+ path: string;
6
+ content: string;
7
+ }[], options: {
8
+ hasTypeScriptConfig: boolean;
9
+ }): Promise<UnusedDependency[]>;
10
+ export declare function runUnusedCheck(context: AnalysisContext): Promise<CheckResult>;
@@ -1,17 +1,20 @@
1
1
  import path from "node:path";
2
- import { collectProjectFiles, readTextFile } from "../utils/file-parser.js";
3
2
  const SOURCE_FILE_PATTERN = /\.(c|m)?(t|j)sx?$/;
4
3
  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
4
  const TEST_FILE_PATTERN = /(^|[\\/])(__tests__|test|tests|spec|specs)([\\/]|$)|\.(test|spec)\.(c|m)?(t|j)sx?$/;
6
5
  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);
9
- const projectFiles = files.filter((filePath) => !filePath.includes(`${path.sep}node_modules${path.sep}`));
6
+ export async function findUnusedDependencies(rootDir, graph, fileEntries, options) {
7
+ const projectFiles = fileEntries
8
+ .map((entry) => entry.path)
9
+ .filter((filePath) => !filePath.includes(`${path.sep}node_modules${path.sep}`));
10
10
  const runtimeUsed = new Set();
11
11
  const devUsed = new Set();
12
- for (const filePath of projectFiles) {
13
- const content = await readTextFile(filePath);
14
- const imports = extractImportedPackages(content);
12
+ for (const entry of fileEntries) {
13
+ if (!SOURCE_FILE_PATTERN.test(entry.path)) {
14
+ continue;
15
+ }
16
+ const imports = extractImportedPackages(entry.content);
17
+ const filePath = entry.path;
15
18
  const isDevOnlyFile = isDevelopmentOnlyFile(rootDir, filePath);
16
19
  const target = isDevOnlyFile ? devUsed : runtimeUsed;
17
20
  for (const importedPackage of imports) {
@@ -25,8 +28,7 @@ export async function findUnusedDependencies(rootDir, graph) {
25
28
  devUsed.add(referencedBinary);
26
29
  }
27
30
  const hasTypeScriptSources = projectFiles.some((filePath) => /\.(c|m)?tsx?$/.test(filePath));
28
- const hasTypeScriptConfig = await hasFile(rootDir, "tsconfig.json");
29
- if (hasTypeScriptConfig) {
31
+ if (options.hasTypeScriptConfig) {
30
32
  devUsed.add("typescript");
31
33
  }
32
34
  const unusedDependencies = Object.keys(graph.dependencies)
@@ -34,10 +36,26 @@ export async function findUnusedDependencies(rootDir, graph) {
34
36
  .map((name) => ({ name, section: "dependencies" }));
35
37
  const unusedDevDependencies = Object.keys(graph.devDependencies)
36
38
  .filter((name) => !devUsed.has(name) && !runtimeUsed.has(name))
37
- .filter((name) => !isImplicitlyUsedDevDependency(name, hasTypeScriptSources, hasTypeScriptConfig))
39
+ .filter((name) => !isImplicitlyUsedDevDependency(name, hasTypeScriptSources, options.hasTypeScriptConfig))
38
40
  .map((name) => ({ name, section: "devDependencies" }));
39
41
  return [...unusedDependencies, ...unusedDevDependencies].sort((left, right) => left.name.localeCompare(right.name));
40
42
  }
43
+ export async function runUnusedCheck(context) {
44
+ const unused = await findUnusedDependencies(context.rootDir, context.graph, context.fileEntries, { hasTypeScriptConfig: context.hasTypeScriptConfig });
45
+ return {
46
+ name: "unused",
47
+ summary: `${unused.length} unused dependencies found`,
48
+ issues: unused.map((item) => ({
49
+ id: `unused:${item.section}:${item.name}`,
50
+ message: `${item.name} appears unused`,
51
+ severity: "warning",
52
+ meta: {
53
+ name: item.name,
54
+ section: item.section
55
+ }
56
+ }))
57
+ };
58
+ }
41
59
  function extractImportedPackages(content) {
42
60
  const imports = new Set();
43
61
  const patterns = [
@@ -109,12 +127,3 @@ function isImplicitlyUsedDevDependency(name, hasTypeScriptSources, hasTypeScript
109
127
  }
110
128
  return false;
111
129
  }
112
- async function hasFile(rootDir, fileName) {
113
- try {
114
- await readTextFile(path.join(rootDir, fileName));
115
- return true;
116
- }
117
- catch {
118
- return false;
119
- }
120
- }
package/dist/cli.js CHANGED
@@ -39,6 +39,30 @@ async function main() {
39
39
  return;
40
40
  }
41
41
  if (command !== "analyze") {
42
+ if (command === "report") {
43
+ const fromPath = optionValues.get("--from") ?? positionals[0];
44
+ if (!fromPath) {
45
+ console.error("Missing --from <file> for report");
46
+ printHelp();
47
+ process.exitCode = 1;
48
+ return;
49
+ }
50
+ try {
51
+ const raw = await fs.readFile(fromPath, "utf8");
52
+ const reportData = JSON.parse(raw);
53
+ const output = flags.has("--json")
54
+ ? JSON.stringify(reportData, null, 2)
55
+ : renderMarkdownReport(reportData);
56
+ await writeOutput(output, optionValues.get("--out"));
57
+ return;
58
+ }
59
+ catch (error) {
60
+ console.error("Failed to render report.");
61
+ console.error(error);
62
+ process.exitCode = 1;
63
+ return;
64
+ }
65
+ }
42
66
  if (command === "config") {
43
67
  if (!(await hasPackageJson(targetPath))) {
44
68
  console.error(`No package.json found at ${targetPath}`);
@@ -78,21 +102,21 @@ async function main() {
78
102
  configPath: optionValues.get("--config"),
79
103
  config: cliConfig
80
104
  });
105
+ let output;
81
106
  if (flags.has("--json")) {
82
- process.stdout.write(`${renderJsonReport(result)}\n`);
107
+ output = renderJsonReport(result);
83
108
  }
84
109
  else if (flags.has("--md")) {
85
- process.stdout.write(`${renderMarkdownReport(result)}\n`);
110
+ output = renderMarkdownReport(result);
86
111
  }
87
112
  else {
88
- const output = renderConsoleReport(result);
89
- if (!output || output.trim().length === 0) {
90
- process.stdout.write(`${renderJsonReport(result)}\n`);
91
- }
92
- else {
93
- process.stdout.write(`${output}\n`);
94
- }
113
+ const consoleOutput = renderConsoleReport(result);
114
+ output =
115
+ !consoleOutput || consoleOutput.trim().length === 0
116
+ ? renderJsonReport(result)
117
+ : consoleOutput;
95
118
  }
119
+ await writeOutput(output, optionValues.get("--out"));
96
120
  if (!result.policy.passed) {
97
121
  process.exitCode = 1;
98
122
  }
@@ -148,7 +172,8 @@ function printHelp() {
148
172
  console.log("Dependency Brain");
149
173
  console.log("");
150
174
  console.log("Usage:");
151
- console.log(" dep-brain analyze [path] [--json] [--md] [--config path] [--min-score n] [--fail-on-risks] [--fail-on-outdated] [--fail-on-unused] [--fail-on-duplicates]");
175
+ console.log(" dep-brain analyze [path] [--json] [--md] [--out path] [--config path] [--min-score n] [--fail-on-risks] [--fail-on-outdated] [--fail-on-unused] [--fail-on-duplicates]");
176
+ console.log(" dep-brain report --from <file> [--md] [--json] [--out path]");
152
177
  console.log(" dep-brain config [path] [--config path]");
153
178
  console.log(" dep-brain help");
154
179
  console.log(" dep-brain --version");
@@ -157,6 +182,8 @@ function printHelp() {
157
182
  console.log(" --json Output JSON for analysis");
158
183
  console.log(" --md Output Markdown report");
159
184
  console.log(" --config <path> Path to depbrain.config.json");
185
+ console.log(" --from <file> Read analysis JSON from file");
186
+ console.log(" --out <path> Write output to a file");
160
187
  console.log(" --min-score <n> Minimum score required to pass");
161
188
  console.log(" --fail-on-risks Fail when risky dependencies exist");
162
189
  console.log(" --fail-on-outdated Fail when outdated dependencies exist");
@@ -176,3 +203,10 @@ async function loadPackageVersion() {
176
203
  return null;
177
204
  }
178
205
  }
206
+ async function writeOutput(output, outPath) {
207
+ if (outPath) {
208
+ await fs.writeFile(outPath, `${output}\n`, "utf8");
209
+ return;
210
+ }
211
+ process.stdout.write(`${output}\n`);
212
+ }
@@ -1,12 +1,13 @@
1
1
  import path from "node:path";
2
- import { findDuplicateDependencies } from "../checks/duplicate.js";
3
- import { findOutdatedDependencies } from "../checks/outdated.js";
4
- import { findRiskDependencies } from "../checks/risk.js";
5
- import { findUnusedDependencies } from "../checks/unused.js";
2
+ import { runDuplicateCheck } from "../checks/duplicate.js";
3
+ import { runOutdatedCheck } from "../checks/outdated.js";
4
+ import { runRiskCheck } from "../checks/risk.js";
5
+ import { runUnusedCheck } from "../checks/unused.js";
6
6
  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
+ import { buildAnalysisContext } from "./context.js";
10
11
  export const OUTPUT_VERSION = "1.0";
11
12
  export async function analyzeProject(options = {}) {
12
13
  const rootDir = path.resolve(options.rootDir ?? process.cwd());
@@ -17,8 +18,9 @@ export async function analyzeProject(options = {}) {
17
18
  return analyzeSingleProject(rootDir, config);
18
19
  }
19
20
  const rootGraph = await buildDependencyGraph(rootDir);
20
- const rawDuplicates = await findDuplicateDependencies(rootGraph);
21
- const duplicates = rawDuplicates.filter((item) => !config.ignore.duplicates.includes(item.name));
21
+ const duplicateCheck = await runDuplicateCheck(rootGraph);
22
+ const filteredDuplicateIssues = filterIssues(duplicateCheck.issues, "duplicates", config);
23
+ const duplicates = mapDuplicateIssues(filteredDuplicateIssues);
22
24
  const packages = [];
23
25
  for (const workspace of workspaces) {
24
26
  const result = await analyzeSingleProject(workspace.rootDir, config, {
@@ -81,7 +83,9 @@ function mergeConfig(base, overrides) {
81
83
  duplicates: overrides.ignore?.duplicates ?? base.ignore.duplicates,
82
84
  outdated: overrides.ignore?.outdated ?? base.ignore.outdated,
83
85
  risks: overrides.ignore?.risks ?? base.ignore.risks,
84
- unused: overrides.ignore?.unused ?? base.ignore.unused
86
+ unused: overrides.ignore?.unused ?? base.ignore.unused,
87
+ prefixes: overrides.ignore?.prefixes ?? base.ignore.prefixes,
88
+ patterns: overrides.ignore?.patterns ?? base.ignore.patterns
85
89
  },
86
90
  policy: {
87
91
  minScore: overrides.policy?.minScore ?? base.policy.minScore,
@@ -98,6 +102,9 @@ function mergeConfig(base, overrides) {
98
102
  outdatedWeight: overrides.scoring?.outdatedWeight ?? base.scoring.outdatedWeight,
99
103
  unusedWeight: overrides.scoring?.unusedWeight ?? base.scoring.unusedWeight,
100
104
  riskWeight: overrides.scoring?.riskWeight ?? base.scoring.riskWeight
105
+ },
106
+ scan: {
107
+ excludePaths: overrides.scan?.excludePaths ?? base.scan.excludePaths
101
108
  }
102
109
  };
103
110
  }
@@ -124,18 +131,13 @@ function evaluatePolicy(summary, config) {
124
131
  };
125
132
  }
126
133
  async function analyzeSingleProject(rootDir, config, options = {}) {
127
- const graph = await buildDependencyGraph(rootDir);
128
- const [rawDuplicates, rawUnused, rawOutdated, rawRisks] = await Promise.all([
129
- findDuplicateDependencies(graph),
130
- findUnusedDependencies(rootDir, graph),
131
- findOutdatedDependencies(graph),
132
- findRiskDependencies(graph)
133
- ]);
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));
134
+ const context = await buildAnalysisContext(rootDir, config);
135
+ const results = await runChecks(context);
136
+ const issueGroups = normalizeIssues(results, config);
137
+ const duplicates = mapDuplicateIssues(issueGroups.duplicates);
138
+ const unused = mapUnusedIssues(issueGroups.unused);
139
+ const outdated = mapOutdatedIssues(issueGroups.outdated);
140
+ const risks = mapRiskIssues(issueGroups.risks);
139
141
  const score = calculateHealthScore({
140
142
  duplicates: duplicates.length,
141
143
  unused: unused.length,
@@ -187,6 +189,106 @@ async function analyzeSingleProject(rootDir, config, options = {}) {
187
189
  config
188
190
  };
189
191
  }
192
+ function shouldIgnorePackage(name, bucket, config) {
193
+ if (config.ignore[bucket].includes(name)) {
194
+ return true;
195
+ }
196
+ if (config.ignore.prefixes.some((prefix) => name.startsWith(prefix))) {
197
+ return true;
198
+ }
199
+ return config.ignore.patterns.some((pattern) => {
200
+ try {
201
+ const regex = new RegExp(pattern);
202
+ return regex.test(name);
203
+ }
204
+ catch {
205
+ return false;
206
+ }
207
+ });
208
+ }
209
+ async function runChecks(context) {
210
+ const checks = [
211
+ {
212
+ name: "duplicate",
213
+ run: () => runDuplicateCheck(context.graph)
214
+ },
215
+ {
216
+ name: "unused",
217
+ run: () => runUnusedCheck(context)
218
+ },
219
+ {
220
+ name: "outdated",
221
+ run: () => runOutdatedCheck(context.graph)
222
+ },
223
+ {
224
+ name: "risk",
225
+ run: () => runRiskCheck(context.graph)
226
+ }
227
+ ];
228
+ const results = [];
229
+ for (const check of checks) {
230
+ results.push(await check.run());
231
+ }
232
+ return results;
233
+ }
234
+ function normalizeIssues(results, config) {
235
+ const map = new Map();
236
+ for (const result of results) {
237
+ map.set(result.name, result.issues);
238
+ }
239
+ return {
240
+ duplicates: filterIssues(map.get("duplicate") ?? [], "duplicates", config),
241
+ unused: filterIssues(map.get("unused") ?? [], "unused", config),
242
+ outdated: filterIssues(map.get("outdated") ?? [], "outdated", config),
243
+ risks: filterIssues(map.get("risk") ?? [], "risks", config)
244
+ };
245
+ }
246
+ function filterIssues(issues, bucket, config) {
247
+ return issues.filter((issue) => {
248
+ const name = typeof issue.meta?.name === "string" ? issue.meta.name : issue.package ?? "";
249
+ if (!name) {
250
+ return true;
251
+ }
252
+ if (bucket === "unused") {
253
+ const section = issue.meta?.section === "devDependencies" ? "devDependencies" : "dependencies";
254
+ if (shouldIgnorePackage(name, section, config)) {
255
+ return false;
256
+ }
257
+ }
258
+ return !shouldIgnorePackage(name, bucket, config);
259
+ });
260
+ }
261
+ function mapDuplicateIssues(issues) {
262
+ return issues.map((issue) => ({
263
+ name: String(issue.meta?.name ?? issue.package ?? "unknown"),
264
+ versions: Array.isArray(issue.meta?.versions) ? issue.meta?.versions : [],
265
+ instances: Array.isArray(issue.meta?.instances)
266
+ ? issue.meta?.instances
267
+ : []
268
+ }));
269
+ }
270
+ function mapUnusedIssues(issues) {
271
+ return issues.map((issue) => ({
272
+ name: String(issue.meta?.name ?? issue.package ?? "unknown"),
273
+ section: issue.meta?.section === "devDependencies" ? "devDependencies" : "dependencies"
274
+ }));
275
+ }
276
+ function mapOutdatedIssues(issues) {
277
+ return issues.map((issue) => ({
278
+ name: String(issue.meta?.name ?? issue.package ?? "unknown"),
279
+ current: String(issue.meta?.current ?? ""),
280
+ latest: String(issue.meta?.latest ?? ""),
281
+ updateType: issue.meta?.updateType === "major" || issue.meta?.updateType === "minor" || issue.meta?.updateType === "patch"
282
+ ? issue.meta.updateType
283
+ : "unknown"
284
+ }));
285
+ }
286
+ function mapRiskIssues(issues) {
287
+ return issues.map((issue) => ({
288
+ name: String(issue.meta?.name ?? issue.package ?? "unknown"),
289
+ reasons: Array.isArray(issue.meta?.reasons) ? issue.meta?.reasons : []
290
+ }));
291
+ }
190
292
  function buildScoreBreakdown(counts, config) {
191
293
  return {
192
294
  baseScore: 100,
@@ -0,0 +1,3 @@
1
+ import type { AnalysisContext } from "./types.js";
2
+ import type { DepBrainConfig } from "../utils/config.js";
3
+ export declare function buildAnalysisContext(rootDir: string, config: DepBrainConfig): Promise<AnalysisContext>;
@@ -0,0 +1,31 @@
1
+ import path from "node:path";
2
+ import { buildDependencyGraph } from "./graph-builder.js";
3
+ import { collectProjectFiles, readTextFile } from "../utils/file-parser.js";
4
+ const SOURCE_FILE_PATTERN = /\.(c|m)?(t|j)sx?$/;
5
+ export async function buildAnalysisContext(rootDir, config) {
6
+ const graph = await buildDependencyGraph(rootDir);
7
+ const projectFiles = await collectProjectFiles(rootDir, SOURCE_FILE_PATTERN, config.scan.excludePaths);
8
+ const fileEntries = await Promise.all(projectFiles.map(async (filePath) => ({
9
+ path: filePath,
10
+ content: await readTextFile(filePath)
11
+ })));
12
+ const sourceText = fileEntries.map((entry) => entry.content).join("\n");
13
+ const hasTypeScriptConfig = await hasFile(rootDir, "tsconfig.json");
14
+ return {
15
+ rootDir,
16
+ graph,
17
+ sourceText,
18
+ projectFiles,
19
+ fileEntries,
20
+ hasTypeScriptConfig
21
+ };
22
+ }
23
+ async function hasFile(rootDir, fileName) {
24
+ try {
25
+ await readTextFile(path.join(rootDir, fileName));
26
+ return true;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
@@ -0,0 +1,28 @@
1
+ export type IssueSeverity = "info" | "warning" | "critical";
2
+ export type Issue = {
3
+ id: string;
4
+ message: string;
5
+ package?: string;
6
+ severity: IssueSeverity;
7
+ meta?: Record<string, unknown>;
8
+ };
9
+ export type CheckResult = {
10
+ name: string;
11
+ issues: Issue[];
12
+ summary: string;
13
+ };
14
+ export type AnalysisContext = {
15
+ rootDir: string;
16
+ graph: import("./graph-builder.js").DependencyGraph;
17
+ sourceText: string;
18
+ projectFiles: string[];
19
+ fileEntries: {
20
+ path: string;
21
+ content: string;
22
+ }[];
23
+ hasTypeScriptConfig: boolean;
24
+ };
25
+ export type CheckRunner = {
26
+ name: string;
27
+ run: (context: AnalysisContext) => Promise<CheckResult>;
28
+ };
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
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
3
  export { OUTPUT_VERSION } from "./core/analyzer.js";
4
+ export type { AnalysisContext, CheckResult, Issue } from "./core/types.js";
4
5
  export type { DepBrainConfig, DepBrainConfigOverrides } from "./utils/config.js";
5
6
  export type { WorkspacePackage } from "./utils/workspaces.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.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "CLI and library for dependency health analysis",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",