impact-analysis 2.0.1 → 2.0.2
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 +9 -1
- package/dist/ai/explain.js +29 -0
- package/dist/cache/graphCache.js +39 -0
- package/dist/cli.js +136 -0
- package/dist/core/analyzer.js +20 -0
- package/dist/git/getChangedFiles.js +21 -0
- package/{src/graph/buildGraph.ts → dist/graph/buildGraph.js} +44 -63
- package/dist/graph/types.js +2 -0
- package/dist/parser/parseJS.js +70 -0
- package/dist/parser/parseVue.js +24 -0
- package/dist/parser/parseVueTemplate.js +15 -0
- package/{src/report/html-generator.ts → dist/report/html-generator.js} +17 -19
- package/dist/scanner/scanRepo.js +30 -0
- package/package.json +32 -1
- package/index.mjs +0 -457
- package/src/ai/explain.ts +0 -29
- package/src/cache/graphCache.ts +0 -49
- package/src/cli.ts +0 -110
- package/src/core/analyzer.ts +0 -28
- package/src/git/getChangedFiles.ts +0 -15
- package/src/graph/types.ts +0 -5
- package/src/parser/parseJS.ts +0 -69
- package/src/parser/parseVue.ts +0 -23
- package/src/parser/parseVueTemplate.ts +0 -16
- package/src/scanner/scanRepo.ts +0 -25
- package/tsconfig.json +0 -10
package/README.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# Impact Analysis
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/impact-analysis)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
|
|
7
|
+
> Analyze the real impact of code changes in JavaScript, TypeScript, React, Vue, and Angular projects.
|
|
8
|
+
|
|
9
|
+
**Keywords:** `impact-analysis` · `dependency-graph` · `code-analysis` · `static-analysis` · `react` · `vue` · `angular` · `typescript` · `ast` · `ast-parser` · `code-impact` · `dependency-tracker` · `git-diff` · `change-analysis`
|
|
10
|
+
|
|
11
|
+
---
|
|
4
12
|
|
|
5
13
|
## Features
|
|
6
14
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.explainImpact = explainImpact;
|
|
4
|
+
const generative_ai_1 = require("@google/generative-ai");
|
|
5
|
+
async function explainImpact(results) {
|
|
6
|
+
if (!process.env.GEMINI_API_KEY) {
|
|
7
|
+
console.log('No GEMINI_API_KEY found. Skipping AI explanation.');
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const genAI = new generative_ai_1.GoogleGenerativeAI(process.env.GEMINI_API_KEY);
|
|
12
|
+
const model = genAI.getGenerativeModel({ model: 'gemini-3-flash-preview' });
|
|
13
|
+
const prompt = `Analyze these code changes and explain their impact:
|
|
14
|
+
|
|
15
|
+
${JSON.stringify(results.map(r => ({
|
|
16
|
+
file: r.changedFile,
|
|
17
|
+
impactedFiles: r.impactedFiles.length,
|
|
18
|
+
risk: r.risk
|
|
19
|
+
})), null, 2)}
|
|
20
|
+
|
|
21
|
+
Provide a brief summary of the overall risk and key concerns.`;
|
|
22
|
+
const result = await model.generateContent(prompt);
|
|
23
|
+
const response = await result.response;
|
|
24
|
+
return response.text();
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
throw new Error(`AI explanation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadCache = loadCache;
|
|
7
|
+
exports.saveCache = saveCache;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const CACHE_FILE = ".impact-analysis-cache.json";
|
|
10
|
+
function loadCache() {
|
|
11
|
+
try {
|
|
12
|
+
if (!fs_1.default.existsSync(CACHE_FILE)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const data = JSON.parse(fs_1.default.readFileSync(CACHE_FILE, "utf-8"));
|
|
16
|
+
return {
|
|
17
|
+
imports: new Map(Object.entries(data.imports).map(([key, value]) => [key, new Set(value)])),
|
|
18
|
+
importedBy: new Map(Object.entries(data.importedBy).map(([key, value]) => [key, new Set(value)])),
|
|
19
|
+
componentUsage: new Map(Object.entries(data.componentUsage).map(([key, value]) => [key, new Set(value)]))
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
console.warn('Cache file corrupted, rebuilding...');
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function saveCache(data) {
|
|
28
|
+
try {
|
|
29
|
+
const serializable = {
|
|
30
|
+
imports: Object.fromEntries(Array.from(data.imports.entries()).map(([key, value]) => [key, Array.from(value)])),
|
|
31
|
+
importedBy: Object.fromEntries(Array.from(data.importedBy.entries()).map(([key, value]) => [key, Array.from(value)])),
|
|
32
|
+
componentUsage: Object.fromEntries(Array.from(data.componentUsage.entries()).map(([key, value]) => [key, Array.from(value)]))
|
|
33
|
+
};
|
|
34
|
+
fs_1.default.writeFileSync(CACHE_FILE, JSON.stringify(serializable, null, 2));
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.warn('Failed to save cache:', error instanceof Error ? error.message : String(error));
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
+
};
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
require("dotenv/config");
|
|
41
|
+
const path_1 = __importDefault(require("path"));
|
|
42
|
+
const child_process_1 = require("child_process");
|
|
43
|
+
const util_1 = require("util");
|
|
44
|
+
const getChangedFiles_1 = require("./git/getChangedFiles");
|
|
45
|
+
const scanRepo_1 = require("./scanner/scanRepo");
|
|
46
|
+
const buildGraph_1 = require("./graph/buildGraph");
|
|
47
|
+
const analyzer_1 = require("./core/analyzer");
|
|
48
|
+
const html_generator_1 = require("./report/html-generator");
|
|
49
|
+
const explain_1 = require("./ai/explain");
|
|
50
|
+
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
51
|
+
async function main() {
|
|
52
|
+
const args = process.argv.slice(2);
|
|
53
|
+
const showHtml = args.includes('--html');
|
|
54
|
+
const useAI = args.includes('--ai');
|
|
55
|
+
const clearCache = args.includes('--clear-cache');
|
|
56
|
+
const baseBranch = args.find(arg => !arg.startsWith('--')) || 'main';
|
|
57
|
+
try {
|
|
58
|
+
if (clearCache) {
|
|
59
|
+
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
60
|
+
if (fs.existsSync('.impact-analysis-cache.json')) {
|
|
61
|
+
fs.unlinkSync('.impact-analysis-cache.json');
|
|
62
|
+
console.log('Cache cleared.');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
console.log('Scanning repository...');
|
|
66
|
+
const allFiles = await (0, scanRepo_1.scanRepo)();
|
|
67
|
+
console.log(`Building dependency graph from ${allFiles.length} files...`);
|
|
68
|
+
const graph = await (0, buildGraph_1.buildGraph)(allFiles);
|
|
69
|
+
console.log(`Getting changed files against ${baseBranch}...`);
|
|
70
|
+
const changedFiles = await (0, getChangedFiles_1.getChangedFiles)(baseBranch);
|
|
71
|
+
if (changedFiles.length === 0) {
|
|
72
|
+
console.log('No changed files found.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
console.log(`Found ${changedFiles.length} changed files. Analyzing impact...`);
|
|
76
|
+
const dependencyMap = {};
|
|
77
|
+
graph.importedBy.forEach((importers, file) => {
|
|
78
|
+
dependencyMap[file] = Array.from(importers);
|
|
79
|
+
});
|
|
80
|
+
const results = (0, analyzer_1.analyzeImpact)(changedFiles, dependencyMap);
|
|
81
|
+
if (useAI) {
|
|
82
|
+
try {
|
|
83
|
+
console.log('Getting AI explanation...');
|
|
84
|
+
const explanation = await (0, explain_1.explainImpact)(results);
|
|
85
|
+
if (explanation) {
|
|
86
|
+
results.aiExplanation = explanation;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
console.warn('AI explanation failed:', error instanceof Error ? error.message : String(error));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
console.log('\n--- Impact Analysis Results ---\n');
|
|
94
|
+
results.forEach((result, index) => {
|
|
95
|
+
console.log(`${index + 1}. ${path_1.default.basename(result.changedFile)}`);
|
|
96
|
+
console.log(` Risk: ${result.risk}`);
|
|
97
|
+
console.log(` Impacted files: ${result.impactedFiles.length}`);
|
|
98
|
+
if (result.impactedFiles.length > 0) {
|
|
99
|
+
result.impactedFiles.slice(0, 5).forEach(file => {
|
|
100
|
+
console.log(` - ${path_1.default.basename(file)}`);
|
|
101
|
+
});
|
|
102
|
+
if (result.impactedFiles.length > 5) {
|
|
103
|
+
console.log(` ... and ${result.impactedFiles.length - 5} more`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
console.log('');
|
|
107
|
+
});
|
|
108
|
+
// Generate HTML by default
|
|
109
|
+
console.log('Generating HTML report...');
|
|
110
|
+
const htmlPath = (0, html_generator_1.generateHtmlReport)(results);
|
|
111
|
+
console.log(`Report saved: ${htmlPath}`);
|
|
112
|
+
if (showHtml) {
|
|
113
|
+
try {
|
|
114
|
+
// Use platform-specific command to open the file
|
|
115
|
+
const command = process.platform === 'win32'
|
|
116
|
+
? `start "" "${htmlPath}"`
|
|
117
|
+
: process.platform === 'darwin'
|
|
118
|
+
? `open "${htmlPath}"`
|
|
119
|
+
: `xdg-open "${htmlPath}"`;
|
|
120
|
+
await execAsync(command);
|
|
121
|
+
console.log(`Report opened in browser`);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
console.log(`Could not auto-open browser. Please open: ${htmlPath}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
console.log(`\nTo view the graphical report, run: impact-analysis --html`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
console.error('Error:', error instanceof Error ? error.message : String(error));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
main();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyzeImpact = analyzeImpact;
|
|
4
|
+
function analyzeImpact(changedFiles, dependencyMap) {
|
|
5
|
+
return changedFiles.map(changed => {
|
|
6
|
+
const impacted = Object.entries(dependencyMap)
|
|
7
|
+
.filter(([, deps]) => deps.includes(changed))
|
|
8
|
+
.map(([file]) => file);
|
|
9
|
+
let risk = 'LOW';
|
|
10
|
+
if (impacted.length > 5)
|
|
11
|
+
risk = 'HIGH';
|
|
12
|
+
else if (impacted.length > 2)
|
|
13
|
+
risk = 'MEDIUM';
|
|
14
|
+
return {
|
|
15
|
+
changedFile: changed,
|
|
16
|
+
impactedFiles: impacted,
|
|
17
|
+
risk
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getChangedFiles = getChangedFiles;
|
|
7
|
+
const simple_git_1 = __importDefault(require("simple-git"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
async function getChangedFiles(base) {
|
|
10
|
+
try {
|
|
11
|
+
const git = (0, simple_git_1.default)();
|
|
12
|
+
const diff = await git.diff(["--name-only", base]);
|
|
13
|
+
return diff
|
|
14
|
+
.split("\n")
|
|
15
|
+
.filter(f => /\.(js|jsx|ts|tsx|vue)$/.test(f))
|
|
16
|
+
.map(f => path_1.default.resolve(process.cwd(), f));
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
throw new Error(`Failed to get changed files. Make sure Git is installed and you're in a Git repository. Base branch: ${base}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -1,52 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildGraph = buildGraph;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const parseJS_1 = require("../parser/parseJS");
|
|
10
|
+
const parseVue_1 = require("../parser/parseVue");
|
|
11
|
+
const graphCache_1 = require("../cache/graphCache");
|
|
12
|
+
function resolveImportPath(importPath, fromFile, allFiles) {
|
|
13
|
+
if ((!importPath.startsWith('.') && !importPath.startsWith('/') &&
|
|
14
|
+
!importPath.startsWith('~') && !importPath.startsWith('@') &&
|
|
15
|
+
!importPath.startsWith('#')) ||
|
|
13
16
|
importPath.startsWith('virtual:') ||
|
|
14
17
|
importPath.startsWith('~icons/') ||
|
|
15
18
|
importPath.startsWith('unplugin-') ||
|
|
16
|
-
/\.(css|scss|sass|less|styl|stylus|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)$/.test(importPath)
|
|
17
|
-
) {
|
|
19
|
+
/\.(css|scss|sass|less|styl|stylus|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)$/.test(importPath)) {
|
|
18
20
|
return null;
|
|
19
21
|
}
|
|
20
|
-
|
|
21
|
-
const baseDir = path.dirname(fromFile);
|
|
22
|
+
const baseDir = path_1.default.dirname(fromFile);
|
|
22
23
|
const projectRoot = process.cwd();
|
|
23
24
|
const extensions = ['.vue', '.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', ''];
|
|
24
25
|
const indexFiles = ['/index.vue', '/index.js', '/index.ts', '/index.jsx', '/index.tsx', '/index.mjs'];
|
|
25
|
-
|
|
26
|
-
let resolvedBase: string | null = null;
|
|
27
|
-
|
|
26
|
+
let resolvedBase = null;
|
|
28
27
|
// Handle various path alias patterns
|
|
29
28
|
if (importPath.startsWith('~/') || importPath.startsWith('@/')) {
|
|
30
29
|
// Common aliases: ~/ or @/ → project root
|
|
31
|
-
resolvedBase =
|
|
32
|
-
}
|
|
30
|
+
resolvedBase = path_1.default.join(projectRoot, importPath.substring(2));
|
|
31
|
+
}
|
|
32
|
+
else if (importPath.startsWith('#')) {
|
|
33
33
|
// Nuxt 3 aliases: #app, #imports, #components
|
|
34
34
|
// Map to common locations
|
|
35
35
|
if (importPath.startsWith('#app') || importPath.startsWith('#imports')) {
|
|
36
36
|
return null; // Skip Nuxt internals
|
|
37
37
|
}
|
|
38
38
|
if (importPath.startsWith('#components/')) {
|
|
39
|
-
resolvedBase =
|
|
40
|
-
}
|
|
39
|
+
resolvedBase = path_1.default.join(projectRoot, 'components', importPath.substring(12));
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
41
42
|
return null;
|
|
42
43
|
}
|
|
43
|
-
}
|
|
44
|
+
}
|
|
45
|
+
else if (importPath.startsWith('/')) {
|
|
44
46
|
// Absolute path from root
|
|
45
|
-
resolvedBase =
|
|
46
|
-
}
|
|
47
|
+
resolvedBase = path_1.default.join(projectRoot, importPath.substring(1));
|
|
48
|
+
}
|
|
49
|
+
else if (importPath.startsWith('.')) {
|
|
47
50
|
// Relative path
|
|
48
|
-
resolvedBase =
|
|
49
|
-
}
|
|
51
|
+
resolvedBase = path_1.default.join(baseDir, importPath);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
50
54
|
// Could be a tsconfig path alias (e.g., @components/...)
|
|
51
55
|
// Try common patterns
|
|
52
56
|
const aliasPatterns = [
|
|
@@ -56,63 +60,49 @@ function resolveImportPath(importPath: string, fromFile: string, allFiles: strin
|
|
|
56
60
|
['@src/', 'src/'],
|
|
57
61
|
['@/', ''],
|
|
58
62
|
];
|
|
59
|
-
|
|
60
63
|
for (const [alias, replacement] of aliasPatterns) {
|
|
61
64
|
if (importPath.startsWith(alias)) {
|
|
62
|
-
resolvedBase =
|
|
65
|
+
resolvedBase = path_1.default.join(projectRoot, importPath.replace(alias, replacement));
|
|
63
66
|
break;
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
|
-
|
|
67
69
|
if (!resolvedBase) {
|
|
68
70
|
return null; // Likely an external package
|
|
69
71
|
}
|
|
70
72
|
}
|
|
71
|
-
|
|
72
73
|
// Normalize path separators
|
|
73
|
-
resolvedBase =
|
|
74
|
-
|
|
74
|
+
resolvedBase = path_1.default.normalize(resolvedBase);
|
|
75
75
|
const hasExtension = extensions.some(ext => ext && importPath.endsWith(ext));
|
|
76
|
-
|
|
77
76
|
for (const ext of extensions) {
|
|
78
77
|
const withExt = hasExtension ? resolvedBase : resolvedBase + ext;
|
|
79
|
-
|
|
80
78
|
if (allFiles.includes(withExt)) {
|
|
81
79
|
return withExt;
|
|
82
80
|
}
|
|
83
81
|
}
|
|
84
|
-
|
|
85
82
|
for (const indexFile of indexFiles) {
|
|
86
83
|
const resolved = resolvedBase + indexFile;
|
|
87
84
|
if (allFiles.includes(resolved)) {
|
|
88
85
|
return resolved;
|
|
89
86
|
}
|
|
90
87
|
}
|
|
91
|
-
|
|
92
88
|
return null;
|
|
93
89
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const cached = loadCache();
|
|
90
|
+
async function buildGraph(files) {
|
|
91
|
+
const cached = (0, graphCache_1.loadCache)();
|
|
97
92
|
if (cached) {
|
|
98
93
|
console.log('Using cached dependency graph...');
|
|
99
94
|
return cached;
|
|
100
95
|
}
|
|
101
|
-
|
|
102
|
-
const graph: DependencyGraph = {
|
|
96
|
+
const graph = {
|
|
103
97
|
imports: new Map(),
|
|
104
98
|
importedBy: new Map(),
|
|
105
99
|
componentUsage: new Map()
|
|
106
100
|
};
|
|
107
|
-
|
|
108
101
|
let filesProcessed = 0;
|
|
109
|
-
|
|
110
102
|
for (const file of files) {
|
|
111
|
-
const code =
|
|
112
|
-
|
|
103
|
+
const code = fs_1.default.readFileSync(file, "utf-8");
|
|
113
104
|
if (file.endsWith(".vue")) {
|
|
114
|
-
const { imports, components } = parseVue(code);
|
|
115
|
-
|
|
105
|
+
const { imports, components } = (0, parseVue_1.parseVue)(code);
|
|
116
106
|
imports.forEach(imp => {
|
|
117
107
|
const resolved = resolveImportPath(imp, file, files);
|
|
118
108
|
if (resolved) {
|
|
@@ -120,16 +110,12 @@ export async function buildGraph(files: string[]): Promise<DependencyGraph> {
|
|
|
120
110
|
graph.importedBy.set(resolved, (graph.importedBy.get(resolved) || new Set()).add(file));
|
|
121
111
|
}
|
|
122
112
|
});
|
|
123
|
-
|
|
124
113
|
components.forEach(component => {
|
|
125
|
-
graph.componentUsage.set(
|
|
126
|
-
component,
|
|
127
|
-
(graph.componentUsage.get(component) || new Set()).add(file)
|
|
128
|
-
);
|
|
114
|
+
graph.componentUsage.set(component, (graph.componentUsage.get(component) || new Set()).add(file));
|
|
129
115
|
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const imports = (0, parseJS_1.extractImports)(code);
|
|
133
119
|
imports.forEach(imp => {
|
|
134
120
|
const resolved = resolveImportPath(imp, file, files);
|
|
135
121
|
if (resolved) {
|
|
@@ -138,14 +124,9 @@ export async function buildGraph(files: string[]): Promise<DependencyGraph> {
|
|
|
138
124
|
}
|
|
139
125
|
});
|
|
140
126
|
}
|
|
141
|
-
|
|
142
127
|
filesProcessed++;
|
|
143
128
|
}
|
|
144
|
-
|
|
145
129
|
console.log(`Dependency graph built: ${graph.importedBy.size} files with dependencies`);
|
|
146
|
-
|
|
147
|
-
saveCache(graph);
|
|
148
|
-
|
|
130
|
+
(0, graphCache_1.saveCache)(graph);
|
|
149
131
|
return graph;
|
|
150
132
|
}
|
|
151
|
-
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.extractImports = extractImports;
|
|
7
|
+
const parser_1 = require("@babel/parser");
|
|
8
|
+
const traverse_1 = __importDefault(require("@babel/traverse"));
|
|
9
|
+
function extractImports(code) {
|
|
10
|
+
const imports = [];
|
|
11
|
+
try {
|
|
12
|
+
const ast = (0, parser_1.parse)(code, {
|
|
13
|
+
sourceType: "unambiguous",
|
|
14
|
+
plugins: [
|
|
15
|
+
"typescript",
|
|
16
|
+
"jsx",
|
|
17
|
+
"decorators-legacy",
|
|
18
|
+
"classProperties",
|
|
19
|
+
"dynamicImport",
|
|
20
|
+
"importMeta",
|
|
21
|
+
"topLevelAwait",
|
|
22
|
+
"classStaticBlock",
|
|
23
|
+
"optionalChaining",
|
|
24
|
+
"nullishCoalescingOperator"
|
|
25
|
+
],
|
|
26
|
+
errorRecovery: true
|
|
27
|
+
});
|
|
28
|
+
(0, traverse_1.default)(ast, {
|
|
29
|
+
// Static imports: import x from 'module'
|
|
30
|
+
ImportDeclaration(path) {
|
|
31
|
+
imports.push(path.node.source.value);
|
|
32
|
+
},
|
|
33
|
+
// Dynamic imports: import('module')
|
|
34
|
+
Import(path) {
|
|
35
|
+
const parent = path.parent;
|
|
36
|
+
if (parent.type === 'CallExpression' && parent.arguments[0]) {
|
|
37
|
+
const arg = parent.arguments[0];
|
|
38
|
+
if (arg.type === 'StringLiteral') {
|
|
39
|
+
imports.push(arg.value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
// Export from: export { x } from 'module'
|
|
44
|
+
ExportNamedDeclaration(path) {
|
|
45
|
+
if (path.node.source) {
|
|
46
|
+
imports.push(path.node.source.value);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
// Export all: export * from 'module'
|
|
50
|
+
ExportAllDeclaration(path) {
|
|
51
|
+
imports.push(path.node.source.value);
|
|
52
|
+
},
|
|
53
|
+
// CommonJS require: const x = require('module')
|
|
54
|
+
CallExpression(path) {
|
|
55
|
+
if (path.node.callee.type === 'Identifier' &&
|
|
56
|
+
path.node.callee.name === 'require' &&
|
|
57
|
+
path.node.arguments[0] &&
|
|
58
|
+
path.node.arguments[0].type === 'StringLiteral') {
|
|
59
|
+
imports.push(path.node.arguments[0].value);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
// Silently skip files with parse errors
|
|
66
|
+
// Most errors are from node_modules or generated files
|
|
67
|
+
}
|
|
68
|
+
// Remove duplicates
|
|
69
|
+
return [...new Set(imports)];
|
|
70
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseVue = parseVue;
|
|
4
|
+
const compiler_sfc_1 = require("@vue/compiler-sfc");
|
|
5
|
+
const parseJS_1 = require("./parseJS");
|
|
6
|
+
const parseVueTemplate_1 = require("./parseVueTemplate");
|
|
7
|
+
function parseVue(code) {
|
|
8
|
+
try {
|
|
9
|
+
const { descriptor } = (0, compiler_sfc_1.parse)(code);
|
|
10
|
+
const scriptContent = descriptor.script?.content || '';
|
|
11
|
+
const scriptSetupContent = descriptor.scriptSetup?.content || '';
|
|
12
|
+
const allScript = scriptContent + '\n' + scriptSetupContent;
|
|
13
|
+
return {
|
|
14
|
+
imports: allScript.trim() ? (0, parseJS_1.extractImports)(allScript) : [],
|
|
15
|
+
components: (0, parseVueTemplate_1.extractTemplateComponents)(code)
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
return {
|
|
20
|
+
imports: [],
|
|
21
|
+
components: []
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractTemplateComponents = extractTemplateComponents;
|
|
4
|
+
const compiler_sfc_1 = require("@vue/compiler-sfc");
|
|
5
|
+
function extractTemplateComponents(code) {
|
|
6
|
+
const { descriptor } = (0, compiler_sfc_1.parse)(code);
|
|
7
|
+
const template = descriptor.template?.content || "";
|
|
8
|
+
const componentRegex = /<([A-Z][\w-]*)/g;
|
|
9
|
+
const components = new Set();
|
|
10
|
+
let match;
|
|
11
|
+
while ((match = componentRegex.exec(template))) {
|
|
12
|
+
components.add(match[1]);
|
|
13
|
+
}
|
|
14
|
+
return Array.from(components);
|
|
15
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateHtmlReport = generateHtmlReport;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
4
9
|
const HTML_TEMPLATE = `<!DOCTYPE html>
|
|
5
10
|
<html>
|
|
6
11
|
<head>
|
|
@@ -324,20 +329,13 @@ const HTML_TEMPLATE = `<!DOCTYPE html>
|
|
|
324
329
|
</script>
|
|
325
330
|
</body>
|
|
326
331
|
</html>`;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const finalHtml = HTML_TEMPLATE.replace(
|
|
337
|
-
'__IMPACT_DATA__',
|
|
338
|
-
JSON.stringify(data)
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
fs.writeFileSync(htmlPath, finalHtml);
|
|
342
|
-
return htmlPath;
|
|
332
|
+
function generateHtmlReport(data) {
|
|
333
|
+
const outDir = path_1.default.resolve('.impact-analysis');
|
|
334
|
+
const jsonPath = path_1.default.join(outDir, 'impact.json');
|
|
335
|
+
const htmlPath = path_1.default.join(outDir, 'report.html');
|
|
336
|
+
fs_1.default.mkdirSync(outDir, { recursive: true });
|
|
337
|
+
fs_1.default.writeFileSync(jsonPath, JSON.stringify(data, null, 2));
|
|
338
|
+
const finalHtml = HTML_TEMPLATE.replace('__IMPACT_DATA__', JSON.stringify(data));
|
|
339
|
+
fs_1.default.writeFileSync(htmlPath, finalHtml);
|
|
340
|
+
return htmlPath;
|
|
343
341
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.scanRepo = scanRepo;
|
|
7
|
+
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
async function scanRepo() {
|
|
10
|
+
try {
|
|
11
|
+
const files = await (0, fast_glob_1.default)("**/*.{js,jsx,ts,tsx,vue}", {
|
|
12
|
+
ignore: [
|
|
13
|
+
"node_modules/**",
|
|
14
|
+
"dist/**",
|
|
15
|
+
"build/**",
|
|
16
|
+
"*.min.js",
|
|
17
|
+
"**/*.min.js",
|
|
18
|
+
".next/**",
|
|
19
|
+
"coverage/**",
|
|
20
|
+
".cache/**",
|
|
21
|
+
"out/**",
|
|
22
|
+
".nuxt/**"
|
|
23
|
+
]
|
|
24
|
+
});
|
|
25
|
+
return files.map(file => path_1.default.resolve(process.cwd(), file));
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw new Error(`Failed to scan repository: ${error instanceof Error ? error.message : String(error)}`);
|
|
29
|
+
}
|
|
30
|
+
}
|