dependency-radar 0.1.1 → 0.2.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 +81 -7
- package/dist/aggregator.js +203 -69
- package/dist/cli.js +24 -15
- package/dist/report-assets.js +18 -0
- package/dist/report.js +15 -1286
- package/dist/runners/importGraphRunner.js +172 -0
- package/dist/utils.js +18 -2
- package/package.json +10 -10
|
@@ -0,0 +1,172 @@
|
|
|
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.runImportGraph = runImportGraph;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
|
+
const module_1 = require("module");
|
|
10
|
+
const utils_1 = require("../utils");
|
|
11
|
+
const IGNORED_DIRS = new Set(['node_modules', 'dist', 'build', 'coverage', '.dependency-radar']);
|
|
12
|
+
const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
13
|
+
async function runImportGraph(projectPath, tempDir) {
|
|
14
|
+
const targetFile = path_1.default.join(tempDir, 'import-graph.json');
|
|
15
|
+
try {
|
|
16
|
+
const srcPath = path_1.default.join(projectPath, 'src');
|
|
17
|
+
const hasSrc = await (0, utils_1.pathExists)(srcPath);
|
|
18
|
+
const entry = hasSrc ? srcPath : projectPath;
|
|
19
|
+
const files = await collectSourceFiles(entry);
|
|
20
|
+
const fileGraph = {};
|
|
21
|
+
const packageGraph = {};
|
|
22
|
+
const unresolvedImports = [];
|
|
23
|
+
for (const file of files) {
|
|
24
|
+
const rel = normalizePath(projectPath, file);
|
|
25
|
+
const content = await promises_1.default.readFile(file, 'utf8');
|
|
26
|
+
const imports = extractImports(content);
|
|
27
|
+
const resolved = await resolveImports(imports, path_1.default.dirname(file), projectPath);
|
|
28
|
+
fileGraph[rel] = resolved.files;
|
|
29
|
+
packageGraph[rel] = resolved.packages;
|
|
30
|
+
unresolvedImports.push(...resolved.unresolved.map((spec) => ({ importer: rel, specifier: spec })));
|
|
31
|
+
}
|
|
32
|
+
const output = { files: fileGraph, packages: packageGraph, unresolvedImports };
|
|
33
|
+
await (0, utils_1.writeJsonFile)(targetFile, output);
|
|
34
|
+
return { ok: true, data: output, file: targetFile };
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
await (0, utils_1.writeJsonFile)(targetFile, { error: String(err) });
|
|
38
|
+
return { ok: false, error: `import graph failed: ${String(err)}`, file: targetFile };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function collectSourceFiles(rootDir) {
|
|
42
|
+
const files = [];
|
|
43
|
+
async function walk(current) {
|
|
44
|
+
const entries = await promises_1.default.readdir(current, { withFileTypes: true });
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (entry.name.startsWith('.'))
|
|
47
|
+
continue;
|
|
48
|
+
const fullPath = path_1.default.join(current, entry.name);
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
if (IGNORED_DIRS.has(entry.name))
|
|
51
|
+
continue;
|
|
52
|
+
await walk(fullPath);
|
|
53
|
+
}
|
|
54
|
+
else if (entry.isFile()) {
|
|
55
|
+
if (SOURCE_EXTENSIONS.includes(path_1.default.extname(entry.name))) {
|
|
56
|
+
files.push(fullPath);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
await walk(rootDir);
|
|
62
|
+
return files;
|
|
63
|
+
}
|
|
64
|
+
function extractImports(content) {
|
|
65
|
+
const matches = new Set();
|
|
66
|
+
const patterns = [
|
|
67
|
+
/\bimport\s+(?:[^'"]+from\s+)?['"]([^'"]+)['"]/g,
|
|
68
|
+
/\bexport\s+(?:[^'"]+from\s+)?['"]([^'"]+)['"]/g,
|
|
69
|
+
/\brequire\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
70
|
+
/\bimport\(\s*['"]([^'"]+)['"]\s*\)/g
|
|
71
|
+
];
|
|
72
|
+
for (const pattern of patterns) {
|
|
73
|
+
let match;
|
|
74
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
75
|
+
if (match[1])
|
|
76
|
+
matches.add(match[1]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return Array.from(matches);
|
|
80
|
+
}
|
|
81
|
+
async function resolveImports(specifiers, fileDir, projectPath) {
|
|
82
|
+
const resolvedFiles = [];
|
|
83
|
+
const resolvedPackages = [];
|
|
84
|
+
const unresolved = [];
|
|
85
|
+
for (const spec of specifiers) {
|
|
86
|
+
if (isBuiltinModule(spec))
|
|
87
|
+
continue;
|
|
88
|
+
if (spec.startsWith('.') || spec.startsWith('/')) {
|
|
89
|
+
const target = await resolveFileTarget(spec, fileDir, projectPath);
|
|
90
|
+
if (target) {
|
|
91
|
+
resolvedFiles.push(target);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
unresolved.push(spec);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
resolvedPackages.push(toPackageName(spec));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
files: uniqSorted(resolvedFiles),
|
|
103
|
+
packages: uniqSorted(resolvedPackages),
|
|
104
|
+
unresolved: uniqSorted(unresolved)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function resolveFileTarget(spec, fileDir, projectPath) {
|
|
108
|
+
const base = spec.startsWith('/')
|
|
109
|
+
? path_1.default.resolve(projectPath, `.${spec}`)
|
|
110
|
+
: path_1.default.resolve(fileDir, spec);
|
|
111
|
+
const direct = await resolveFile(base);
|
|
112
|
+
if (direct)
|
|
113
|
+
return normalizePath(projectPath, direct);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
async function resolveFile(basePath) {
|
|
117
|
+
if (await isFile(basePath))
|
|
118
|
+
return basePath;
|
|
119
|
+
for (const ext of SOURCE_EXTENSIONS) {
|
|
120
|
+
const candidate = `${basePath}${ext}`;
|
|
121
|
+
if (await isFile(candidate))
|
|
122
|
+
return candidate;
|
|
123
|
+
}
|
|
124
|
+
if (await isDir(basePath)) {
|
|
125
|
+
for (const ext of SOURCE_EXTENSIONS) {
|
|
126
|
+
const candidate = path_1.default.join(basePath, `index${ext}`);
|
|
127
|
+
if (await isFile(candidate))
|
|
128
|
+
return candidate;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
async function isFile(target) {
|
|
134
|
+
try {
|
|
135
|
+
const stat = await promises_1.default.stat(target);
|
|
136
|
+
return stat.isFile();
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function isDir(target) {
|
|
143
|
+
try {
|
|
144
|
+
const stat = await promises_1.default.stat(target);
|
|
145
|
+
return stat.isDirectory();
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function toPackageName(spec) {
|
|
152
|
+
if (spec.startsWith('@')) {
|
|
153
|
+
const parts = spec.split('/');
|
|
154
|
+
return parts.slice(0, 2).join('/');
|
|
155
|
+
}
|
|
156
|
+
return spec.split('/')[0];
|
|
157
|
+
}
|
|
158
|
+
function normalizePath(baseDir, filePath) {
|
|
159
|
+
const rel = path_1.default.relative(baseDir, filePath);
|
|
160
|
+
return rel.split(path_1.default.sep).join('/');
|
|
161
|
+
}
|
|
162
|
+
const BUILTIN_MODULES = new Set(module_1.builtinModules.flatMap((mod) => (mod.startsWith('node:') ? [mod, mod.slice(5)] : [mod])));
|
|
163
|
+
function isBuiltinModule(spec) {
|
|
164
|
+
const normalized = spec.startsWith('node:') ? spec.slice(5) : spec;
|
|
165
|
+
if (BUILTIN_MODULES.has(spec) || BUILTIN_MODULES.has(normalized))
|
|
166
|
+
return true;
|
|
167
|
+
const root = normalized.split('/')[0];
|
|
168
|
+
return BUILTIN_MODULES.has(root);
|
|
169
|
+
}
|
|
170
|
+
function uniqSorted(values) {
|
|
171
|
+
return Array.from(new Set(values)).sort();
|
|
172
|
+
}
|
package/dist/utils.js
CHANGED
|
@@ -138,11 +138,27 @@ async function readLicenseFromPackageJson(pkgName, projectPath) {
|
|
|
138
138
|
const pkgRaw = await promises_1.default.readFile(pkgJsonPath, 'utf8');
|
|
139
139
|
const pkg = JSON.parse(pkgRaw);
|
|
140
140
|
const license = pkg.license || (Array.isArray(pkg.licenses) ? pkg.licenses.map((l) => (typeof l === 'string' ? l : l === null || l === void 0 ? void 0 : l.type)).filter(Boolean).join(' OR ') : undefined);
|
|
141
|
-
|
|
141
|
+
const licenseFile = await findLicenseFile(path_1.default.dirname(pkgJsonPath));
|
|
142
|
+
if (!license && !licenseFile)
|
|
142
143
|
return undefined;
|
|
143
|
-
return { license, licenseFile
|
|
144
|
+
return { license, licenseFile };
|
|
144
145
|
}
|
|
145
146
|
catch (err) {
|
|
146
147
|
return undefined;
|
|
147
148
|
}
|
|
148
149
|
}
|
|
150
|
+
async function findLicenseFile(dir) {
|
|
151
|
+
try {
|
|
152
|
+
const entries = await promises_1.default.readdir(dir, { withFileTypes: true });
|
|
153
|
+
const fileNames = entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
154
|
+
const patterns = [/^licen[cs]e(\.|$)/, /^copying(\.|$)/, /^notice(\.|$)/];
|
|
155
|
+
const match = fileNames.find((name) => {
|
|
156
|
+
const lower = name.toLowerCase();
|
|
157
|
+
return patterns.some((pattern) => pattern.test(lower));
|
|
158
|
+
});
|
|
159
|
+
return match ? path_1.default.join(dir, match) : undefined;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dependency-radar",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Local-first dependency analysis tool that generates a single HTML report showing risk, size, usage, and structure of your project
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Local-first dependency analysis tool that generates a single HTML report showing risk, size, usage, and structure of your project's dependencies.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"dependency-radar": "dist/cli.js"
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
"dist"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"
|
|
13
|
+
"dev:report": "cd report-ui && npx vite",
|
|
14
|
+
"build:report-ui": "cd report-ui && npx vite build",
|
|
15
|
+
"build:report": "npm run build:report-ui && npx ts-node scripts/build-report.ts",
|
|
16
|
+
"build": "npm run build:report && tsc",
|
|
14
17
|
"dev": "ts-node src/cli.ts scan",
|
|
15
18
|
"scan": "node dist/cli.js scan",
|
|
16
19
|
"prepublishOnly": "npm run build",
|
|
@@ -19,7 +22,7 @@
|
|
|
19
22
|
"author": " ",
|
|
20
23
|
"license": "MIT",
|
|
21
24
|
"engines": {
|
|
22
|
-
"node": ">=
|
|
25
|
+
"node": ">=14.14.0"
|
|
23
26
|
},
|
|
24
27
|
"repository": {
|
|
25
28
|
"type": "git",
|
|
@@ -43,14 +46,11 @@
|
|
|
43
46
|
"node",
|
|
44
47
|
"html"
|
|
45
48
|
],
|
|
46
|
-
"dependencies": {
|
|
47
|
-
"depcheck": "^1.4.7",
|
|
48
|
-
"license-checker": "^25.0.1",
|
|
49
|
-
"madge": "^8.0.0"
|
|
50
|
-
},
|
|
51
49
|
"devDependencies": {
|
|
52
50
|
"@types/node": "^20.11.30",
|
|
51
|
+
"terser": "^5.27.0",
|
|
53
52
|
"ts-node": "^10.9.2",
|
|
54
|
-
"typescript": "^5.4.3"
|
|
53
|
+
"typescript": "^5.4.3",
|
|
54
|
+
"vite": "^5.4.0"
|
|
55
55
|
}
|
|
56
56
|
}
|