@aiready/context-analyzer 0.22.14 → 0.22.16
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/.turbo/turbo-build.log +19 -19
- package/.turbo/turbo-format-check.log +1 -1
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-test.log +347 -25
- package/.turbo/turbo-type-check.log +1 -1
- package/dist/chunk-2LEHY2GV.mjs +1287 -0
- package/dist/chunk-P2ZQGQAO.mjs +1282 -0
- package/dist/chunk-QGI23DBA.mjs +1282 -0
- package/dist/chunk-QTB4KYCX.mjs +1260 -0
- package/dist/chunk-RQ5BQLT6.mjs +102 -0
- package/dist/chunk-VYFHSGV6.mjs +1283 -0
- package/dist/chunk-WLXLBWDU.mjs +96 -0
- package/dist/chunk-XDYPMFCH.mjs +1250 -0
- package/dist/cli-action-332WE54N.mjs +95 -0
- package/dist/cli-action-7QXG7LHS.mjs +95 -0
- package/dist/cli-action-BIX6TYXF.mjs +95 -0
- package/dist/cli-action-BUGVCH44.mjs +95 -0
- package/dist/cli-action-JKG3R6RV.mjs +93 -0
- package/dist/cli-action-RO24U52W.mjs +95 -0
- package/dist/cli-action-WAZ5KM6X.mjs +95 -0
- package/dist/cli-action-XDKINE2R.mjs +95 -0
- package/dist/cli-action-Y6VATXMV.mjs +95 -0
- package/dist/cli.js +79 -31
- package/dist/cli.mjs +1 -1
- package/dist/console-report-CVGRMWEU.mjs +74 -0
- package/dist/html-report-BYGKWC3K.mjs +73 -0
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +71 -25
- package/dist/index.mjs +3 -3
- package/dist/orchestrator-2KQNMO2L.mjs +10 -0
- package/dist/orchestrator-66ZVNOLR.mjs +10 -0
- package/dist/orchestrator-KM2OJPZD.mjs +10 -0
- package/dist/orchestrator-MKDZPRBA.mjs +10 -0
- package/dist/orchestrator-QSHWWBWS.mjs +10 -0
- package/dist/orchestrator-WFQPMNSD.mjs +10 -0
- package/dist/python-context-H2OLC5JN.mjs +162 -0
- package/dist/python-context-OBP7JD5P.mjs +162 -0
- package/package.json +8 -6
- package/src/__tests__/analyzer.test.ts +4 -3
- package/src/__tests__/issue-analyzer.test.ts +4 -2
- package/src/classify/file-classifiers.ts +14 -13
- package/src/cli-action.ts +6 -3
- package/src/graph-builder.ts +43 -8
- package/src/issue-analyzer.ts +19 -7
- package/src/orchestrator.ts +6 -4
- package/src/report/console-report.ts +2 -2
- package/src/report/html-report.ts +2 -2
- package/src/semantic/domain-inference.ts +1 -1
- package/src/types.ts +2 -0
- package/src/utils/dependency-graph-utils.ts +22 -13
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DependencyNode } from '../types';
|
|
2
|
+
import type { ExportInfo } from '@aiready/core';
|
|
2
3
|
import {
|
|
3
4
|
BARREL_EXPORT_MIN_EXPORTS,
|
|
4
5
|
BARREL_EXPORT_TOKEN_LIMIT,
|
|
@@ -23,7 +24,7 @@ export function isBoilerplateBarrel(node: DependencyNode): boolean {
|
|
|
23
24
|
if (!exports || exports.length === 0) return false;
|
|
24
25
|
|
|
25
26
|
// 1. Must be purely re-exports
|
|
26
|
-
const isPurelyReexports = exports.every((exp:
|
|
27
|
+
const isPurelyReexports = exports.every((exp: ExportInfo) => !!exp.source);
|
|
27
28
|
if (!isPurelyReexports) return false;
|
|
28
29
|
|
|
29
30
|
// 2. Must be low local token cost (no actual logic)
|
|
@@ -31,7 +32,7 @@ export function isBoilerplateBarrel(node: DependencyNode): boolean {
|
|
|
31
32
|
|
|
32
33
|
// 3. Detect "Architectural Theater"
|
|
33
34
|
// If it re-exports everything from exactly ONE source, it's a pass-through
|
|
34
|
-
const sources = new Set(exports.map((exp:
|
|
35
|
+
const sources = new Set(exports.map((exp: ExportInfo) => exp.source));
|
|
35
36
|
|
|
36
37
|
// Pattern: export * from '../actual'
|
|
37
38
|
const isSingleSourcePassThrough = sources.size === 1;
|
|
@@ -63,7 +64,7 @@ export function isBarrelExport(node: DependencyNode): boolean {
|
|
|
63
64
|
|
|
64
65
|
const isReexportPattern =
|
|
65
66
|
(exports || []).length >= BARREL_EXPORT_MIN_EXPORTS &&
|
|
66
|
-
(exports || []).every((exp:
|
|
67
|
+
(exports || []).every((exp: ExportInfo) =>
|
|
67
68
|
['const', 'function', 'type', 'interface'].includes(exp.type)
|
|
68
69
|
);
|
|
69
70
|
|
|
@@ -86,7 +87,7 @@ export function isTypeDefinition(node: DependencyNode): boolean {
|
|
|
86
87
|
const areAllTypes =
|
|
87
88
|
hasExports &&
|
|
88
89
|
nodeExports.every(
|
|
89
|
-
(exp:
|
|
90
|
+
(exp: ExportInfo) => exp.type === 'type' || exp.type === 'interface'
|
|
90
91
|
);
|
|
91
92
|
|
|
92
93
|
const isTypePath = /\/(types|interfaces|models)\//i.test(file);
|
|
@@ -124,7 +125,7 @@ export function isLambdaHandler(node: DependencyNode): boolean {
|
|
|
124
125
|
);
|
|
125
126
|
const isHandlerPath = /\/(handlers|lambdas|lambda|functions)\//i.test(file);
|
|
126
127
|
const hasHandlerExport = (exports || []).some(
|
|
127
|
-
(exp:
|
|
128
|
+
(exp: ExportInfo) =>
|
|
128
129
|
['handler', 'main', 'lambdahandler'].includes(exp.name.toLowerCase()) ||
|
|
129
130
|
exp.name.toLowerCase().endsWith('handler')
|
|
130
131
|
);
|
|
@@ -146,11 +147,11 @@ export function isServiceFile(node: DependencyNode): boolean {
|
|
|
146
147
|
fileName.includes(pattern)
|
|
147
148
|
);
|
|
148
149
|
const isServicePath = file.toLowerCase().includes('/services/');
|
|
149
|
-
const hasServiceNamedExport = (exports || []).some((exp:
|
|
150
|
+
const hasServiceNamedExport = (exports || []).some((exp: ExportInfo) =>
|
|
150
151
|
exp.name.toLowerCase().includes('service')
|
|
151
152
|
);
|
|
152
153
|
const hasClassExport = (exports || []).some(
|
|
153
|
-
(exp:
|
|
154
|
+
(exp: ExportInfo) => exp.type === 'class'
|
|
154
155
|
);
|
|
155
156
|
return (
|
|
156
157
|
isServiceName || isServicePath || (hasServiceNamedExport && hasClassExport)
|
|
@@ -173,7 +174,7 @@ export function isEmailTemplate(node: DependencyNode): boolean {
|
|
|
173
174
|
);
|
|
174
175
|
const isEmailPath = /\/(emails|mail|notifications)\//i.test(file);
|
|
175
176
|
const hasTemplateFunction = (exports || []).some(
|
|
176
|
-
(exp:
|
|
177
|
+
(exp: ExportInfo) =>
|
|
177
178
|
exp.type === 'function' &&
|
|
178
179
|
(exp.name.toLowerCase().startsWith('render') ||
|
|
179
180
|
exp.name.toLowerCase().startsWith('generate'))
|
|
@@ -197,7 +198,7 @@ export function isParserFile(node: DependencyNode): boolean {
|
|
|
197
198
|
);
|
|
198
199
|
const isParserPath = /\/(parsers|transformers)\//i.test(file);
|
|
199
200
|
const hasParseFunction = (exports || []).some(
|
|
200
|
-
(exp:
|
|
201
|
+
(exp: ExportInfo) =>
|
|
201
202
|
exp.type === 'function' &&
|
|
202
203
|
(exp.name.toLowerCase().startsWith('parse') ||
|
|
203
204
|
exp.name.toLowerCase().startsWith('transform'))
|
|
@@ -220,7 +221,7 @@ export function isSessionFile(node: DependencyNode): boolean {
|
|
|
220
221
|
fileName.includes(pattern)
|
|
221
222
|
);
|
|
222
223
|
const isSessionPath = /\/(sessions|state)\//i.test(file);
|
|
223
|
-
const hasSessionExport = (exports || []).some((exp:
|
|
224
|
+
const hasSessionExport = (exports || []).some((exp: ExportInfo) =>
|
|
224
225
|
['session', 'state', 'store'].some((pattern: string) =>
|
|
225
226
|
exp.name.toLowerCase().includes(pattern)
|
|
226
227
|
)
|
|
@@ -246,10 +247,10 @@ export function isNextJsPage(node: DependencyNode): boolean {
|
|
|
246
247
|
return false;
|
|
247
248
|
|
|
248
249
|
const hasDefaultExport = (exports || []).some(
|
|
249
|
-
(exp:
|
|
250
|
+
(exp: ExportInfo) => exp.type === 'default'
|
|
250
251
|
);
|
|
251
252
|
|
|
252
|
-
const hasNextJsExport = (exports || []).some((exp:
|
|
253
|
+
const hasNextJsExport = (exports || []).some((exp: ExportInfo) =>
|
|
253
254
|
NEXTJS_METADATA_EXPORTS.includes(exp.name.toLowerCase())
|
|
254
255
|
);
|
|
255
256
|
|
|
@@ -272,7 +273,7 @@ export function isConfigFile(node: DependencyNode): boolean {
|
|
|
272
273
|
fileName.includes(pattern)
|
|
273
274
|
);
|
|
274
275
|
const isConfigPath = /\/(config|settings|schemas)\//i.test(lowerPath);
|
|
275
|
-
const hasSchemaExport = (exports || []).some((exp:
|
|
276
|
+
const hasSchemaExport = (exports || []).some((exp: ExportInfo) =>
|
|
276
277
|
['schema', 'config', 'setting'].some((pattern: string) =>
|
|
277
278
|
exp.name.toLowerCase().includes(pattern)
|
|
278
279
|
)
|
package/src/cli-action.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from '@aiready/core';
|
|
8
8
|
import { analyzeContext } from './orchestrator';
|
|
9
9
|
import { generateSummary } from './summary';
|
|
10
|
+
import type { ContextAnalyzerOptions } from './types';
|
|
10
11
|
import chalk from 'chalk';
|
|
11
12
|
import { writeFileSync } from 'fs';
|
|
12
13
|
|
|
@@ -28,7 +29,7 @@ export async function contextActionHandler(directory: string, options: any) {
|
|
|
28
29
|
try {
|
|
29
30
|
// Define defaults
|
|
30
31
|
const defaults = {
|
|
31
|
-
maxDepth:
|
|
32
|
+
maxDepth: 10,
|
|
32
33
|
maxContextBudget: 10000,
|
|
33
34
|
minCohesion: 0.6,
|
|
34
35
|
maxFragmentation: 0.5,
|
|
@@ -68,7 +69,9 @@ export async function contextActionHandler(directory: string, options: any) {
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
// Run analysis
|
|
71
|
-
const results = await analyzeContext(
|
|
72
|
+
const results = await analyzeContext(
|
|
73
|
+
finalOptions as ContextAnalyzerOptions
|
|
74
|
+
);
|
|
72
75
|
const summary = generateSummary(results, finalOptions);
|
|
73
76
|
|
|
74
77
|
const duration = getElapsedTime(startTime);
|
|
@@ -104,7 +107,7 @@ export async function contextActionHandler(directory: string, options: any) {
|
|
|
104
107
|
} else {
|
|
105
108
|
// Default: Console (Dynamic Import)
|
|
106
109
|
const { displayConsoleReport } = await import('./report/console-report');
|
|
107
|
-
displayConsoleReport(summary, results,
|
|
110
|
+
displayConsoleReport(summary, results, finalOptions.maxResults);
|
|
108
111
|
console.log(chalk.dim(`\n✨ Analysis completed in ${duration}ms\n`));
|
|
109
112
|
}
|
|
110
113
|
} catch (error) {
|
package/src/graph-builder.ts
CHANGED
|
@@ -23,6 +23,35 @@ function resolveImport(
|
|
|
23
23
|
// If it's not a relative import, we treat it as an external dependency for now
|
|
24
24
|
// (unless it's an absolute path that exists in our set)
|
|
25
25
|
if (!source.startsWith('.') && !source.startsWith('/')) {
|
|
26
|
+
// Ignore standard libraries and external packages we don't control
|
|
27
|
+
const externalIgnores = [
|
|
28
|
+
'react',
|
|
29
|
+
'next',
|
|
30
|
+
'lucide-react',
|
|
31
|
+
'framer-motion',
|
|
32
|
+
'@aws-sdk',
|
|
33
|
+
'stripe',
|
|
34
|
+
'clsx',
|
|
35
|
+
'tailwind-merge',
|
|
36
|
+
'zod',
|
|
37
|
+
'commander',
|
|
38
|
+
'chalk',
|
|
39
|
+
'fs',
|
|
40
|
+
'path',
|
|
41
|
+
'util',
|
|
42
|
+
'child_process',
|
|
43
|
+
'os',
|
|
44
|
+
'crypto',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
externalIgnores.some(
|
|
49
|
+
(pkg) => source === pkg || source.startsWith(`${pkg}/`)
|
|
50
|
+
)
|
|
51
|
+
) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
26
55
|
// Handle monorepo package imports (@aiready/*)
|
|
27
56
|
if (source.startsWith('@aiready/')) {
|
|
28
57
|
const pkgName = source.split('/')[1];
|
|
@@ -144,12 +173,15 @@ export async function buildDependencyGraph(
|
|
|
144
173
|
// 1. Get high-fidelity AST-based imports & exports
|
|
145
174
|
const { imports: astImports } = await parseFileExports(content, file);
|
|
146
175
|
|
|
147
|
-
// 2.
|
|
148
|
-
const
|
|
176
|
+
// 2. Filter out type-only imports (they don't create runtime dependencies)
|
|
177
|
+
const runtimeImports = astImports.filter((i) => !i.isTypeOnly);
|
|
178
|
+
|
|
179
|
+
// 3. Resolve imports to absolute paths in the graph
|
|
180
|
+
const resolvedImports = runtimeImports
|
|
149
181
|
.map((i) => resolveImport(i.source, file, allFilePaths))
|
|
150
182
|
.filter((path): path is string => path !== null);
|
|
151
183
|
|
|
152
|
-
const importSources =
|
|
184
|
+
const importSources = runtimeImports.map((i) => i.source);
|
|
153
185
|
|
|
154
186
|
// 3. Wrap with platform-specific metadata (v0.11+)
|
|
155
187
|
const exports = await extractExportsWithAST(
|
|
@@ -233,7 +265,7 @@ export function getTransitiveDependencies(
|
|
|
233
265
|
file: string,
|
|
234
266
|
graph: DependencyGraph,
|
|
235
267
|
visited = new Set<string>()
|
|
236
|
-
): string
|
|
268
|
+
): Map<string, number> {
|
|
237
269
|
return getTransitiveDependenciesFromEdges(file, graph.edges, visited);
|
|
238
270
|
}
|
|
239
271
|
|
|
@@ -242,7 +274,7 @@ export function getTransitiveDependencies(
|
|
|
242
274
|
*
|
|
243
275
|
* @param file - File path to calculate budget for.
|
|
244
276
|
* @param graph - The dependency graph.
|
|
245
|
-
* @returns Total token count including recursive dependencies.
|
|
277
|
+
* @returns Total token count including recursive dependencies (discounted by depth).
|
|
246
278
|
*/
|
|
247
279
|
export function calculateContextBudget(
|
|
248
280
|
file: string,
|
|
@@ -254,14 +286,17 @@ export function calculateContextBudget(
|
|
|
254
286
|
let totalTokens = node.tokenCost;
|
|
255
287
|
const deps = getTransitiveDependencies(file, graph);
|
|
256
288
|
|
|
257
|
-
for (const dep of deps) {
|
|
289
|
+
for (const [dep, depth] of deps.entries()) {
|
|
258
290
|
const depNode = graph.nodes.get(dep);
|
|
259
291
|
if (depNode) {
|
|
260
|
-
|
|
292
|
+
// Discount token cost by depth (20% reduction per level)
|
|
293
|
+
// This prevents "barrel file" false positives where a facade pulls in the entire project
|
|
294
|
+
const discountFactor = Math.pow(0.8, depth - 1);
|
|
295
|
+
totalTokens += depNode.tokenCost * discountFactor;
|
|
261
296
|
}
|
|
262
297
|
}
|
|
263
298
|
|
|
264
|
-
return totalTokens;
|
|
299
|
+
return Math.round(totalTokens);
|
|
265
300
|
}
|
|
266
301
|
|
|
267
302
|
/**
|
package/src/issue-analyzer.ts
CHANGED
|
@@ -17,6 +17,7 @@ export { isBuildArtifact };
|
|
|
17
17
|
export function analyzeIssues(params: {
|
|
18
18
|
file: string;
|
|
19
19
|
importDepth: number;
|
|
20
|
+
tokenCost: number;
|
|
20
21
|
contextBudget: number;
|
|
21
22
|
cohesionScore: number;
|
|
22
23
|
fragmentationScore: number;
|
|
@@ -34,6 +35,7 @@ export function analyzeIssues(params: {
|
|
|
34
35
|
const {
|
|
35
36
|
file,
|
|
36
37
|
importDepth,
|
|
38
|
+
tokenCost,
|
|
37
39
|
contextBudget,
|
|
38
40
|
cohesionScore,
|
|
39
41
|
fragmentationScore,
|
|
@@ -74,22 +76,32 @@ export function analyzeIssues(params: {
|
|
|
74
76
|
potentialSavings += contextBudget * 0.15;
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
// Check
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
// Check direct file size
|
|
80
|
+
const MAX_FILE_TOKENS = 10000;
|
|
81
|
+
if (tokenCost > MAX_FILE_TOKENS) {
|
|
82
|
+
if (severity !== Severity.Critical) severity = Severity.Major;
|
|
80
83
|
issues.push(
|
|
81
|
-
`
|
|
84
|
+
`File is excessively large (${tokenCost.toLocaleString()} tokens)`
|
|
82
85
|
);
|
|
83
86
|
recommendations.push(
|
|
84
|
-
'Split into smaller modules
|
|
87
|
+
'Split file into smaller, single-responsibility modules'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check transitive context budget
|
|
92
|
+
if (contextBudget > maxContextBudget * 1.5) {
|
|
93
|
+
severity = Severity.Critical;
|
|
94
|
+
issues.push(
|
|
95
|
+
`Total context budget ${contextBudget.toLocaleString()} tokens is 50% over limit`
|
|
85
96
|
);
|
|
97
|
+
recommendations.push('Reduce dependency tree width or reduce deep imports');
|
|
86
98
|
potentialSavings += contextBudget * 0.4;
|
|
87
99
|
} else if (contextBudget > maxContextBudget) {
|
|
88
100
|
if (severity !== Severity.Critical) severity = Severity.Major;
|
|
89
101
|
issues.push(
|
|
90
|
-
`
|
|
102
|
+
`Total context budget ${contextBudget.toLocaleString()} exceeds ${maxContextBudget.toLocaleString()}`
|
|
91
103
|
);
|
|
92
|
-
recommendations.push('
|
|
104
|
+
recommendations.push('Optimize dependency graph and reduce deep imports');
|
|
93
105
|
potentialSavings += contextBudget * 0.2;
|
|
94
106
|
}
|
|
95
107
|
|
package/src/orchestrator.ts
CHANGED
|
@@ -76,6 +76,7 @@ function mapNodeToResult(
|
|
|
76
76
|
{
|
|
77
77
|
file,
|
|
78
78
|
importDepth,
|
|
79
|
+
tokenCost,
|
|
79
80
|
contextBudget,
|
|
80
81
|
cohesionScore,
|
|
81
82
|
fragmentationScore,
|
|
@@ -102,8 +103,8 @@ function mapNodeToResult(
|
|
|
102
103
|
tokenCost,
|
|
103
104
|
linesOfCode: node.linesOfCode,
|
|
104
105
|
importDepth,
|
|
105
|
-
dependencyCount: transitiveDeps.
|
|
106
|
-
dependencyList: transitiveDeps,
|
|
106
|
+
dependencyCount: transitiveDeps.size,
|
|
107
|
+
dependencyList: Array.from(transitiveDeps.keys()),
|
|
107
108
|
circularDeps,
|
|
108
109
|
cohesionScore,
|
|
109
110
|
domains: Array.from(
|
|
@@ -166,8 +167,8 @@ export async function analyzeContext(
|
|
|
166
167
|
...scanOptions,
|
|
167
168
|
exclude:
|
|
168
169
|
includeNodeModules && scanOptions.exclude
|
|
169
|
-
? scanOptions.exclude.filter(
|
|
170
|
-
(pattern) => pattern !== '**/node_modules/**'
|
|
170
|
+
? (scanOptions.exclude as string[]).filter(
|
|
171
|
+
(pattern: string) => pattern !== '**/node_modules/**'
|
|
171
172
|
)
|
|
172
173
|
: scanOptions.exclude,
|
|
173
174
|
});
|
|
@@ -197,6 +198,7 @@ export async function analyzeContext(
|
|
|
197
198
|
analyzeIssues({
|
|
198
199
|
file: metric.file,
|
|
199
200
|
importDepth: metric.importDepth,
|
|
201
|
+
tokenCost: metric.contextBudget, // For Python, we use the reported budget as self-cost for now
|
|
200
202
|
contextBudget: metric.contextBudget,
|
|
201
203
|
cohesionScore: metric.cohesion,
|
|
202
204
|
fragmentationScore: 0,
|
|
@@ -99,12 +99,12 @@ export function displayConsoleReport(
|
|
|
99
99
|
console.log(chalk.cyan(divider));
|
|
100
100
|
console.log(
|
|
101
101
|
chalk.dim(
|
|
102
|
-
'\n⭐ Like aiready? Star us on GitHub: https://github.com/
|
|
102
|
+
'\n⭐ Like aiready? Star us on GitHub: https://github.com/getaiready/aiready-context-analyzer'
|
|
103
103
|
)
|
|
104
104
|
);
|
|
105
105
|
console.log(
|
|
106
106
|
chalk.dim(
|
|
107
|
-
'🐛 Found a bug? Report it: https://github.com/
|
|
107
|
+
'🐛 Found a bug? Report it: https://github.com/getaiready/aiready-context-analyzer/issues\n'
|
|
108
108
|
)
|
|
109
109
|
);
|
|
110
110
|
}
|
|
@@ -74,8 +74,8 @@ export function generateHTMLReport(
|
|
|
74
74
|
{
|
|
75
75
|
title: 'Context Analysis Report',
|
|
76
76
|
packageName: 'context-analyzer',
|
|
77
|
-
packageUrl: 'https://github.com/
|
|
78
|
-
bugUrl: 'https://github.com/
|
|
77
|
+
packageUrl: 'https://github.com/getaiready/aiready-context-analyzer',
|
|
78
|
+
bugUrl: 'https://github.com/getaiready/aiready-context-analyzer/issues',
|
|
79
79
|
emoji: '🧠',
|
|
80
80
|
},
|
|
81
81
|
stats,
|
package/src/types.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface ContextAnalyzerOptions extends ScanOptions {
|
|
|
18
18
|
focus?: 'fragmentation' | 'cohesion' | 'depth' | 'all';
|
|
19
19
|
/** Whether to include node_modules in the analysis (default: false) */
|
|
20
20
|
includeNodeModules?: boolean;
|
|
21
|
+
/** Maximum number of results to display in console output (default: 10) */
|
|
22
|
+
maxResults?: number;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
/**
|
|
@@ -31,24 +31,33 @@ export function getTransitiveDependenciesFromEdges(
|
|
|
31
31
|
file: string,
|
|
32
32
|
edges: Map<string, Set<string>>,
|
|
33
33
|
visited = new Set<string>()
|
|
34
|
-
): string
|
|
35
|
-
|
|
34
|
+
): Map<string, number> {
|
|
35
|
+
const result = new Map<string, number>();
|
|
36
|
+
|
|
37
|
+
function dfs(current: string, depth: number) {
|
|
38
|
+
if (visited.has(current)) {
|
|
39
|
+
const existingDepth = result.get(current);
|
|
40
|
+
if (existingDepth !== undefined && depth < existingDepth) {
|
|
41
|
+
result.set(current, depth);
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
visited.add(current);
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
if (current !== file) {
|
|
48
|
+
result.set(current, depth);
|
|
49
|
+
}
|
|
39
50
|
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
const dependencies = edges.get(current);
|
|
52
|
+
if (!dependencies) return;
|
|
42
53
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
allDeps.push(
|
|
47
|
-
...getTransitiveDependenciesFromEdges(dep, edges, nextVisited)
|
|
48
|
-
);
|
|
54
|
+
for (const dep of dependencies) {
|
|
55
|
+
dfs(dep, depth + 1);
|
|
56
|
+
}
|
|
49
57
|
}
|
|
50
58
|
|
|
51
|
-
|
|
59
|
+
dfs(file, 0);
|
|
60
|
+
return result;
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
export function detectGraphCycles(edges: Map<string, Set<string>>): string[][] {
|