vibeclean 1.0.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.
@@ -0,0 +1,294 @@
1
+ import path from "node:path";
2
+ import { severityFromScore, scoreFromRatio } from "./utils.js";
3
+
4
+ const EXTENSIONS = [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".vue", ".svelte"];
5
+
6
+ function resolveRelativeImport(fromFile, specifier, existingFiles) {
7
+ const baseDir = path.dirname(fromFile);
8
+ const direct = path.normalize(path.join(baseDir, specifier));
9
+
10
+ if (existingFiles.has(direct)) {
11
+ return direct;
12
+ }
13
+
14
+ for (const ext of EXTENSIONS) {
15
+ if (existingFiles.has(`${direct}${ext}`)) {
16
+ return `${direct}${ext}`;
17
+ }
18
+ }
19
+
20
+ for (const ext of EXTENSIONS) {
21
+ if (existingFiles.has(path.join(direct, `index${ext}`))) {
22
+ return path.join(direct, `index${ext}`);
23
+ }
24
+ }
25
+
26
+ return null;
27
+ }
28
+
29
+ function extractRelativeImports(content) {
30
+ const imports = [];
31
+
32
+ for (const match of content.matchAll(/import\s+[^"'`]*?from\s+["'`]([^"'`]+)["'`]/g)) {
33
+ imports.push(match[1]);
34
+ }
35
+ for (const match of content.matchAll(/import\s+["'`]([^"'`]+)["'`]/g)) {
36
+ imports.push(match[1]);
37
+ }
38
+ for (const match of content.matchAll(/require\(\s*["'`]([^"'`]+)["'`]\s*\)/g)) {
39
+ imports.push(match[1]);
40
+ }
41
+
42
+ return imports.filter((item) => item.startsWith("."));
43
+ }
44
+
45
+ function extractImportUsage(content) {
46
+ const imports = [];
47
+
48
+ for (const match of content.matchAll(/import\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?:,\s*\{([^}]+)\})?\s+from\s+["'`]([^"'`]+)["'`]/g)) {
49
+ const names = (match[2] || "")
50
+ .split(",")
51
+ .map((item) => item.trim().split(/\s+as\s+/i)[0])
52
+ .filter(Boolean);
53
+ imports.push({ specifier: match[3], names, defaultImport: true, namespaceImport: false });
54
+ }
55
+
56
+ for (const match of content.matchAll(/import\s+\{([^}]+)\}\s+from\s+["'`]([^"'`]+)["'`]/g)) {
57
+ const names = match[1]
58
+ .split(",")
59
+ .map((item) => item.trim().split(/\s+as\s+/i)[0])
60
+ .filter(Boolean);
61
+ imports.push({ specifier: match[2], names, defaultImport: false, namespaceImport: false });
62
+ }
63
+
64
+ for (const match of content.matchAll(/import\s+\*\s+as\s+[A-Za-z_$][A-Za-z0-9_$]*\s+from\s+["'`]([^"'`]+)["'`]/g)) {
65
+ imports.push({ specifier: match[1], names: [], defaultImport: false, namespaceImport: true });
66
+ }
67
+
68
+ for (const match of content.matchAll(/const\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*require\(\s*["'`]([^"'`]+)["'`]\s*\)/g)) {
69
+ imports.push({ specifier: match[1], names: [], defaultImport: true, namespaceImport: false });
70
+ }
71
+
72
+ for (const match of content.matchAll(/const\s+\{([^}]+)\}\s*=\s*require\(\s*["'`]([^"'`]+)["'`]\s*\)/g)) {
73
+ const names = match[1]
74
+ .split(",")
75
+ .map((item) => item.trim().split(/\s*:\s*/)[0])
76
+ .filter(Boolean);
77
+ imports.push({ specifier: match[2], names, defaultImport: false, namespaceImport: false });
78
+ }
79
+
80
+ return imports;
81
+ }
82
+
83
+ function extractExports(content) {
84
+ const names = new Set();
85
+ let hasDefault = /\bexport\s+default\b/.test(content);
86
+
87
+ for (const match of content.matchAll(/export\s+(?:const|let|var|function|class)\s+([A-Za-z_$][A-Za-z0-9_$]*)/g)) {
88
+ names.add(match[1]);
89
+ }
90
+
91
+ for (const match of content.matchAll(/export\s*\{([^}]+)\}/g)) {
92
+ const parts = match[1]
93
+ .split(",")
94
+ .map((item) => item.trim())
95
+ .filter(Boolean);
96
+ for (const part of parts) {
97
+ const aliasMatch = part.match(/^([A-Za-z_$][A-Za-z0-9_$]*)(?:\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*))?$/i);
98
+ if (!aliasMatch) {
99
+ continue;
100
+ }
101
+
102
+ const localName = aliasMatch[1];
103
+ const exportedName = aliasMatch[2] || localName;
104
+ if (exportedName === "default") {
105
+ hasDefault = true;
106
+ } else {
107
+ names.add(localName);
108
+ }
109
+ }
110
+ }
111
+
112
+ return {
113
+ named: [...names],
114
+ hasDefault
115
+ };
116
+ }
117
+
118
+ function isEntrypoint(relativePath) {
119
+ return (
120
+ /(^|\/)(index|main|app)\.(js|jsx|ts|tsx|mjs|cjs)$/.test(relativePath) ||
121
+ /(^|\/)(pages|routes)\//.test(relativePath)
122
+ );
123
+ }
124
+
125
+ function codeLineCount(content) {
126
+ const lines = content.split("\n");
127
+ let count = 0;
128
+
129
+ for (const line of lines) {
130
+ const trimmed = line.trim();
131
+ if (!trimmed || trimmed.startsWith("//") || trimmed === "{" || trimmed === "}") {
132
+ continue;
133
+ }
134
+ count += 1;
135
+ }
136
+
137
+ return count;
138
+ }
139
+
140
+ function isReexportOnly(content) {
141
+ const lines = content
142
+ .split("\n")
143
+ .map((line) => line.trim())
144
+ .filter(Boolean);
145
+
146
+ if (!lines.length) {
147
+ return false;
148
+ }
149
+
150
+ return lines.every((line) => /^export\s+(\*|\{)/.test(line));
151
+ }
152
+
153
+ export function analyzeDeadCode(files) {
154
+ const existingFiles = new Set(files.map((file) => file.relativePath));
155
+ const incomingRefs = new Map();
156
+ const exportsByFile = new Map();
157
+ const importUsageByFile = new Map();
158
+
159
+ for (const file of files) {
160
+ incomingRefs.set(file.relativePath, new Set());
161
+ }
162
+
163
+ for (const file of files) {
164
+ const imports = extractRelativeImports(file.content);
165
+ for (const specifier of imports) {
166
+ const resolved = resolveRelativeImport(file.relativePath, specifier, existingFiles);
167
+ if (!resolved || !incomingRefs.has(resolved)) {
168
+ continue;
169
+ }
170
+
171
+ incomingRefs.get(resolved).add(file.relativePath);
172
+ }
173
+
174
+ exportsByFile.set(file.relativePath, extractExports(file.content));
175
+
176
+ for (const importInfo of extractImportUsage(file.content)) {
177
+ const resolved = importInfo.specifier.startsWith(".")
178
+ ? resolveRelativeImport(file.relativePath, importInfo.specifier, existingFiles)
179
+ : null;
180
+
181
+ if (!resolved) {
182
+ continue;
183
+ }
184
+
185
+ if (!importUsageByFile.has(resolved)) {
186
+ importUsageByFile.set(resolved, {
187
+ named: new Set(),
188
+ defaultImport: false,
189
+ namespaceImport: false
190
+ });
191
+ }
192
+
193
+ const usage = importUsageByFile.get(resolved);
194
+ usage.defaultImport = usage.defaultImport || Boolean(importInfo.defaultImport);
195
+ usage.namespaceImport = usage.namespaceImport || Boolean(importInfo.namespaceImport);
196
+ for (const name of importInfo.names) {
197
+ usage.named.add(name);
198
+ }
199
+ }
200
+ }
201
+
202
+ const orphanFiles = [];
203
+ for (const file of files) {
204
+ const refs = incomingRefs.get(file.relativePath);
205
+ const inSrc = file.relativePath.startsWith("src/");
206
+
207
+ if (inSrc && refs && refs.size === 0 && !isEntrypoint(file.relativePath)) {
208
+ orphanFiles.push(file.relativePath);
209
+ }
210
+ }
211
+
212
+ const unusedExports = [];
213
+ for (const [filePath, exportInfo] of exportsByFile.entries()) {
214
+ if (!exportInfo.named.length && !exportInfo.hasDefault) {
215
+ continue;
216
+ }
217
+
218
+ const used = importUsageByFile.get(filePath) || {
219
+ named: new Set(),
220
+ defaultImport: false,
221
+ namespaceImport: false
222
+ };
223
+
224
+ for (const exportName of exportInfo.named) {
225
+ if (!used.named.has(exportName) && !used.namespaceImport && !isEntrypoint(filePath)) {
226
+ unusedExports.push({ file: filePath, name: exportName });
227
+ }
228
+ }
229
+
230
+ if (exportInfo.hasDefault && !used.defaultImport && !used.namespaceImport && !isEntrypoint(filePath)) {
231
+ unusedExports.push({ file: filePath, name: "default" });
232
+ }
233
+ }
234
+
235
+ const stubFiles = [];
236
+ for (const file of files) {
237
+ const lines = codeLineCount(file.content);
238
+ if (lines < 5 || isReexportOnly(file.content)) {
239
+ stubFiles.push({ file: file.relativePath, lines });
240
+ }
241
+ }
242
+
243
+ const findings = [];
244
+ if (orphanFiles.length) {
245
+ findings.push({
246
+ severity: orphanFiles.length > 5 ? "high" : "medium",
247
+ message: `${orphanFiles.length} orphan files are not imported anywhere.`,
248
+ files: orphanFiles.slice(0, 25)
249
+ });
250
+ }
251
+
252
+ if (unusedExports.length) {
253
+ findings.push({
254
+ severity: "medium",
255
+ message: `${unusedExports.length} exports are never imported.`,
256
+ files: unusedExports.map((item) => item.file).slice(0, 25)
257
+ });
258
+ }
259
+
260
+ if (stubFiles.length) {
261
+ findings.push({
262
+ severity: "low",
263
+ message: `${stubFiles.length} files look like stubs or thin re-export shells.`,
264
+ files: stubFiles.map((item) => item.file).slice(0, 25)
265
+ });
266
+ }
267
+
268
+ const signal = orphanFiles.length * 2 + unusedExports.length + stubFiles.length;
269
+ const score = Math.min(10, scoreFromRatio(signal / Math.max(files.length * 0.6, 1), 10));
270
+
271
+ return {
272
+ id: "deadcode",
273
+ title: "DEAD CODE",
274
+ score,
275
+ severity: severityFromScore(score),
276
+ totalIssues: orphanFiles.length + unusedExports.length + stubFiles.length,
277
+ summary:
278
+ findings.length > 0
279
+ ? `${findings.length} dead-code signals found.`
280
+ : "No major dead-code hotspots detected.",
281
+ metrics: {
282
+ orphanFiles,
283
+ unusedExports,
284
+ stubFiles
285
+ },
286
+ recommendations: [
287
+ "Delete or wire orphan files into active code paths.",
288
+ "Remove unused exports or consume them where needed.",
289
+ "Consolidate stub/re-export files where they do not add structure."
290
+ ],
291
+ findings
292
+ };
293
+ }
294
+
@@ -0,0 +1,241 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { collectImportSpecifiers, packageRoot, severityFromScore, scoreFromRatio } from "./utils.js";
4
+
5
+ const DUPLICATE_GROUPS = [
6
+ ["lodash", "underscore"],
7
+ ["moment", "dayjs"],
8
+ ["moment", "date-fns"],
9
+ ["express", "koa"],
10
+ ["express", "fastify"],
11
+ ["jest", "vitest"],
12
+ ["jest", "mocha"],
13
+ ["vitest", "mocha"]
14
+ ];
15
+
16
+ const OUTDATED_PACKAGES = {
17
+ moment: "Consider migrating to dayjs or date-fns for lighter bundles.",
18
+ request: "request is deprecated. Prefer fetch or undici.",
19
+ lodash: "Consider lodash-es or native methods where possible."
20
+ };
21
+
22
+ const ESTIMATED_MB = {
23
+ lodash: 0.5,
24
+ moment: 0.6,
25
+ request: 0.3,
26
+ axios: 0.2,
27
+ express: 1.0,
28
+ jest: 1.7,
29
+ mocha: 0.8,
30
+ chalk: 0.08,
31
+ uuid: 0.05,
32
+ cors: 0.04,
33
+ dotenv: 0.03
34
+ };
35
+
36
+ const CLI_PACKAGE_ALIASES = {
37
+ jest: "jest",
38
+ vitest: "vitest",
39
+ mocha: "mocha",
40
+ eslint: "eslint",
41
+ prettier: "prettier",
42
+ tsc: "typescript",
43
+ vite: "vite",
44
+ webpack: "webpack",
45
+ rollup: "rollup",
46
+ nodemon: "nodemon",
47
+ ava: "ava",
48
+ nyc: "nyc",
49
+ "ts-node": "ts-node"
50
+ };
51
+
52
+ const CONFIG_FILE_HINTS = {
53
+ tailwindcss: ["tailwind.config.js", "tailwind.config.cjs", "tailwind.config.mjs"],
54
+ postcss: ["postcss.config.js", "postcss.config.cjs", "postcss.config.mjs"],
55
+ autoprefixer: ["postcss.config.js", "postcss.config.cjs", "postcss.config.mjs"],
56
+ "@babel/core": ["babel.config.js", ".babelrc", ".babelrc.json"],
57
+ eslint: [".eslintrc", ".eslintrc.json", ".eslintrc.js", "eslint.config.js"],
58
+ prettier: [".prettierrc", ".prettierrc.json", "prettier.config.js"],
59
+ jest: ["jest.config.js", "jest.config.cjs", "jest.config.mjs"],
60
+ vitest: ["vitest.config.js", "vitest.config.ts", "vite.config.js", "vite.config.ts"]
61
+ };
62
+
63
+ const SAFE_DEV_TOOLS = new Set([
64
+ "typescript",
65
+ "@types/node",
66
+ "@types/react",
67
+ "@types/react-dom",
68
+ "husky",
69
+ "lint-staged",
70
+ "rimraf",
71
+ "cross-env",
72
+ "npm-run-all",
73
+ "concurrently"
74
+ ]);
75
+
76
+ async function exists(rootDir, relativeFile) {
77
+ try {
78
+ await fs.access(path.join(rootDir, relativeFile));
79
+ return true;
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+
85
+ function parseScriptCommands(scripts = {}) {
86
+ const used = new Set();
87
+
88
+ for (const script of Object.values(scripts)) {
89
+ const firstCommand = script.split(/&&|\|\||;/)[0].trim();
90
+ const parts = firstCommand.split(/\s+/).filter(Boolean);
91
+ const token = parts[0] || "";
92
+ let normalized = token;
93
+
94
+ if (token === "npx") {
95
+ normalized = parts[1] || "";
96
+ } else if ((token === "pnpm" || token === "yarn" || token === "npm") && parts[1] === "run") {
97
+ normalized = parts[2] || "";
98
+ }
99
+
100
+ if (CLI_PACKAGE_ALIASES[normalized]) {
101
+ used.add(CLI_PACKAGE_ALIASES[normalized]);
102
+ }
103
+ }
104
+
105
+ return used;
106
+ }
107
+
108
+ export async function analyzeDependencies(files, context = {}) {
109
+ const packageJson = context.packageJson;
110
+ const rootDir = context.rootDir;
111
+
112
+ if (!packageJson) {
113
+ return {
114
+ id: "dependencies",
115
+ title: "DEPENDENCY ISSUES",
116
+ score: 0,
117
+ severity: "low",
118
+ totalIssues: 0,
119
+ summary: "No package.json found. Dependency analysis was skipped.",
120
+ metrics: {},
121
+ recommendations: ["Add a package.json if you want dependency auditing."],
122
+ findings: [],
123
+ skipped: true
124
+ };
125
+ }
126
+
127
+ const dependencies = packageJson.dependencies || {};
128
+ const devDependencies = packageJson.devDependencies || {};
129
+ const allDeps = [...Object.keys(dependencies), ...Object.keys(devDependencies)];
130
+
131
+ const importedPackages = new Set();
132
+ for (const file of files) {
133
+ for (const specifier of collectImportSpecifiers(file.content)) {
134
+ const root = packageRoot(specifier);
135
+ if (root) {
136
+ importedPackages.add(root);
137
+ }
138
+ }
139
+ }
140
+
141
+ const scriptUsedPackages = parseScriptCommands(packageJson.scripts || {});
142
+
143
+ const configHintUsed = new Set();
144
+ for (const [pkg, configFiles] of Object.entries(CONFIG_FILE_HINTS)) {
145
+ for (const configFile of configFiles) {
146
+ if (await exists(rootDir, configFile)) {
147
+ configHintUsed.add(pkg);
148
+ break;
149
+ }
150
+ }
151
+ }
152
+
153
+ const unused = [];
154
+ for (const dep of allDeps) {
155
+ const usedByImport = importedPackages.has(dep);
156
+ const usedByScript = scriptUsedPackages.has(dep);
157
+ const usedByConfig = configHintUsed.has(dep);
158
+ const safeTool = SAFE_DEV_TOOLS.has(dep) || dep.startsWith("@types/");
159
+
160
+ if (!usedByImport && !usedByScript && !usedByConfig && !safeTool) {
161
+ unused.push(dep);
162
+ }
163
+ }
164
+
165
+ const duplicates = [];
166
+ for (const group of DUPLICATE_GROUPS) {
167
+ const present = group.filter((name) => allDeps.includes(name));
168
+ if (present.length > 1) {
169
+ duplicates.push(present);
170
+ }
171
+ }
172
+
173
+ const outdated = [];
174
+ for (const dep of allDeps) {
175
+ if (OUTDATED_PACKAGES[dep]) {
176
+ outdated.push({ dep, note: OUTDATED_PACKAGES[dep] });
177
+ }
178
+ }
179
+
180
+ const estimatedSavingsMb = unused.reduce((sum, dep) => sum + (ESTIMATED_MB[dep] || 0.05), 0);
181
+
182
+ const findings = [];
183
+ if (unused.length) {
184
+ findings.push({
185
+ severity: unused.length >= 5 ? "high" : "medium",
186
+ message: `${unused.length} unused packages detected.`,
187
+ packages: unused.slice(0, 30)
188
+ });
189
+ }
190
+
191
+ if (duplicates.length) {
192
+ findings.push({
193
+ severity: "medium",
194
+ message: `${duplicates.length} duplicate functionality groups found.`,
195
+ packages: duplicates
196
+ });
197
+ }
198
+
199
+ if (outdated.length) {
200
+ findings.push({
201
+ severity: "medium",
202
+ message: `${outdated.length} outdated or heavy packages detected.`,
203
+ packages: outdated.map((item) => item.dep)
204
+ });
205
+ }
206
+
207
+ const score = Math.min(
208
+ 10,
209
+ scoreFromRatio(
210
+ (unused.length + duplicates.length * 2 + outdated.length) / Math.max(allDeps.length * 0.5, 1),
211
+ 10
212
+ )
213
+ );
214
+
215
+ return {
216
+ id: "dependencies",
217
+ title: "DEPENDENCY ISSUES",
218
+ score,
219
+ severity: severityFromScore(score),
220
+ totalIssues: unused.length + duplicates.length + outdated.length,
221
+ summary:
222
+ findings.length > 0
223
+ ? `${findings.length} dependency risk areas found.`
224
+ : "No major dependency bloat or overlap detected.",
225
+ metrics: {
226
+ dependencyCount: Object.keys(dependencies).length,
227
+ devDependencyCount: Object.keys(devDependencies).length,
228
+ unusedCount: unused.length,
229
+ duplicateGroups: duplicates,
230
+ outdated: outdated,
231
+ estimatedSavingsMb: Number(estimatedSavingsMb.toFixed(2))
232
+ },
233
+ recommendations: [
234
+ "Remove unused dependencies to reduce install time and attack surface.",
235
+ "Keep one package per concern where possible (date, test runner, web framework).",
236
+ "Replace deprecated packages with actively maintained alternatives."
237
+ ],
238
+ findings
239
+ };
240
+ }
241
+