swynx-lite 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.
- package/README.md +113 -0
- package/bin/swynx-lite +3 -0
- package/package.json +47 -0
- package/src/clean.mjs +280 -0
- package/src/cli.mjs +264 -0
- package/src/config.mjs +121 -0
- package/src/output/console.mjs +298 -0
- package/src/output/json.mjs +76 -0
- package/src/output/progress.mjs +57 -0
- package/src/scan.mjs +143 -0
- package/src/security.mjs +62 -0
- package/src/shared/fixer/barrel-cleaner.mjs +192 -0
- package/src/shared/fixer/import-cleaner.mjs +237 -0
- package/src/shared/fixer/quarantine.mjs +218 -0
- package/src/shared/scanner/analysers/buildSystems.mjs +647 -0
- package/src/shared/scanner/analysers/configParsers.mjs +1086 -0
- package/src/shared/scanner/analysers/deadcode.mjs +6194 -0
- package/src/shared/scanner/analysers/entryPointDetector.mjs +634 -0
- package/src/shared/scanner/analysers/generatedCode.mjs +297 -0
- package/src/shared/scanner/analysers/imports.mjs +60 -0
- package/src/shared/scanner/discovery.mjs +240 -0
- package/src/shared/scanner/parse-worker.mjs +82 -0
- package/src/shared/scanner/parsers/assets.mjs +44 -0
- package/src/shared/scanner/parsers/csharp.mjs +400 -0
- package/src/shared/scanner/parsers/css.mjs +60 -0
- package/src/shared/scanner/parsers/go.mjs +445 -0
- package/src/shared/scanner/parsers/java.mjs +364 -0
- package/src/shared/scanner/parsers/javascript.mjs +823 -0
- package/src/shared/scanner/parsers/kotlin.mjs +350 -0
- package/src/shared/scanner/parsers/python.mjs +497 -0
- package/src/shared/scanner/parsers/registry.mjs +233 -0
- package/src/shared/scanner/parsers/rust.mjs +427 -0
- package/src/shared/scanner/scan-dead-code.mjs +316 -0
- package/src/shared/security/patterns.mjs +349 -0
- package/src/shared/security/proximity.mjs +84 -0
- package/src/shared/security/scanner.mjs +269 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
// src/scanner/analysers/buildSystems.mjs
|
|
2
|
+
// Enterprise build system detection for monorepos and multi-language projects
|
|
3
|
+
|
|
4
|
+
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
5
|
+
import { join, dirname, basename } from 'path';
|
|
6
|
+
import { globSync } from 'glob';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Detected build system info
|
|
10
|
+
* @typedef {Object} BuildSystemInfo
|
|
11
|
+
* @property {string} type - Build system type (gradle, maven, bazel, etc.)
|
|
12
|
+
* @property {string} configFile - Path to config file
|
|
13
|
+
* @property {string[]} packages - Detected package/module directories
|
|
14
|
+
* @property {Object} metadata - Additional metadata
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Detect all build systems in a project
|
|
19
|
+
* @param {string} projectPath - Path to project root
|
|
20
|
+
* @returns {BuildSystemInfo[]} - Array of detected build systems
|
|
21
|
+
*/
|
|
22
|
+
export function detectBuildSystems(projectPath) {
|
|
23
|
+
if (!projectPath || !existsSync(projectPath)) return [];
|
|
24
|
+
|
|
25
|
+
const systems = [];
|
|
26
|
+
|
|
27
|
+
// JavaScript/TypeScript (already partially handled)
|
|
28
|
+
systems.push(...detectTurborepo(projectPath));
|
|
29
|
+
|
|
30
|
+
// JVM
|
|
31
|
+
systems.push(...detectGradle(projectPath));
|
|
32
|
+
systems.push(...detectMaven(projectPath));
|
|
33
|
+
|
|
34
|
+
// Bazel/Buck/Pants
|
|
35
|
+
systems.push(...detectBazel(projectPath));
|
|
36
|
+
systems.push(...detectBuck(projectPath));
|
|
37
|
+
systems.push(...detectPants(projectPath));
|
|
38
|
+
|
|
39
|
+
// Go
|
|
40
|
+
systems.push(...detectGoWorkspace(projectPath));
|
|
41
|
+
|
|
42
|
+
// .NET
|
|
43
|
+
systems.push(...detectDotNet(projectPath));
|
|
44
|
+
|
|
45
|
+
// Rust
|
|
46
|
+
systems.push(...detectCargo(projectPath));
|
|
47
|
+
|
|
48
|
+
// Python
|
|
49
|
+
systems.push(...detectPythonProject(projectPath));
|
|
50
|
+
|
|
51
|
+
return systems;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get all package directories from detected build systems
|
|
56
|
+
* @param {string} projectPath - Path to project root
|
|
57
|
+
* @returns {string[]} - Array of package directory paths (relative)
|
|
58
|
+
*/
|
|
59
|
+
export function getPackageDirectories(projectPath) {
|
|
60
|
+
const systems = detectBuildSystems(projectPath);
|
|
61
|
+
const dirs = new Set();
|
|
62
|
+
|
|
63
|
+
for (const system of systems) {
|
|
64
|
+
for (const pkg of system.packages || []) {
|
|
65
|
+
dirs.add(pkg);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return [...dirs];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
73
|
+
// Turborepo
|
|
74
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
75
|
+
|
|
76
|
+
function detectTurborepo(projectPath) {
|
|
77
|
+
const turboPath = join(projectPath, 'turbo.json');
|
|
78
|
+
if (!existsSync(turboPath)) return [];
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const content = readFileSync(turboPath, 'utf-8');
|
|
82
|
+
const turbo = JSON.parse(content);
|
|
83
|
+
|
|
84
|
+
// Turborepo uses package.json workspaces for packages
|
|
85
|
+
// turbo.json defines the pipeline
|
|
86
|
+
const pipelines = Object.keys(turbo.pipeline || turbo.tasks || {});
|
|
87
|
+
|
|
88
|
+
return [{
|
|
89
|
+
type: 'turborepo',
|
|
90
|
+
configFile: 'turbo.json',
|
|
91
|
+
packages: [], // Packages come from package.json workspaces
|
|
92
|
+
metadata: { pipelines }
|
|
93
|
+
}];
|
|
94
|
+
} catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
100
|
+
// Gradle (Java/Kotlin)
|
|
101
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
102
|
+
|
|
103
|
+
function detectGradle(projectPath) {
|
|
104
|
+
const settingsFiles = ['settings.gradle', 'settings.gradle.kts'];
|
|
105
|
+
const results = [];
|
|
106
|
+
|
|
107
|
+
for (const settingsFile of settingsFiles) {
|
|
108
|
+
const settingsPath = join(projectPath, settingsFile);
|
|
109
|
+
if (!existsSync(settingsPath)) continue;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const content = readFileSync(settingsPath, 'utf-8');
|
|
113
|
+
const packages = [];
|
|
114
|
+
|
|
115
|
+
// Parse include statements
|
|
116
|
+
// include ':app', ':core', ':shared:utils'
|
|
117
|
+
// include(":app", ":core")
|
|
118
|
+
const includePatterns = [
|
|
119
|
+
/include\s*\(\s*['"]([^'"]+)['"]/g, // include(":app")
|
|
120
|
+
/include\s+['"]([^'"]+)['"]/g, // include ':app'
|
|
121
|
+
/include\s*\(\s*([^)]+)\)/g, // include(":app", ":core")
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
for (const pattern of includePatterns) {
|
|
125
|
+
let match;
|
|
126
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
127
|
+
const modules = match[1].split(/[,\s]+/).map(m =>
|
|
128
|
+
m.replace(/['"]/g, '').replace(/^:/, '').replace(/:/g, '/')
|
|
129
|
+
).filter(m => m);
|
|
130
|
+
packages.push(...modules);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Parse includeFlat
|
|
135
|
+
const flatPattern = /includeFlat\s+['"]([^'"]+)['"]/g;
|
|
136
|
+
let match;
|
|
137
|
+
while ((match = flatPattern.exec(content)) !== null) {
|
|
138
|
+
packages.push(`../${match[1]}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
results.push({
|
|
142
|
+
type: 'gradle',
|
|
143
|
+
configFile: settingsFile,
|
|
144
|
+
packages: [...new Set(packages)],
|
|
145
|
+
metadata: { isKotlinDsl: settingsFile.endsWith('.kts') }
|
|
146
|
+
});
|
|
147
|
+
} catch {
|
|
148
|
+
// Ignore parse errors
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return results;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
156
|
+
// Maven (Java)
|
|
157
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
158
|
+
|
|
159
|
+
function detectMaven(projectPath) {
|
|
160
|
+
const pomPath = join(projectPath, 'pom.xml');
|
|
161
|
+
if (!existsSync(pomPath)) return [];
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const content = readFileSync(pomPath, 'utf-8');
|
|
165
|
+
const packages = [];
|
|
166
|
+
|
|
167
|
+
// Parse <modules> section
|
|
168
|
+
// <modules>
|
|
169
|
+
// <module>core</module>
|
|
170
|
+
// <module>api</module>
|
|
171
|
+
// </modules>
|
|
172
|
+
const modulesMatch = content.match(/<modules>([\s\S]*?)<\/modules>/);
|
|
173
|
+
if (modulesMatch) {
|
|
174
|
+
const modulePattern = /<module>([^<]+)<\/module>/g;
|
|
175
|
+
let match;
|
|
176
|
+
while ((match = modulePattern.exec(modulesMatch[1])) !== null) {
|
|
177
|
+
packages.push(match[1].trim());
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Recursively check for submodule pom.xml files
|
|
182
|
+
for (const pkg of [...packages]) {
|
|
183
|
+
const subPomPath = join(projectPath, pkg, 'pom.xml');
|
|
184
|
+
if (existsSync(subPomPath)) {
|
|
185
|
+
try {
|
|
186
|
+
const subContent = readFileSync(subPomPath, 'utf-8');
|
|
187
|
+
const subModulesMatch = subContent.match(/<modules>([\s\S]*?)<\/modules>/);
|
|
188
|
+
if (subModulesMatch) {
|
|
189
|
+
const modulePattern = /<module>([^<]+)<\/module>/g;
|
|
190
|
+
let match;
|
|
191
|
+
while ((match = modulePattern.exec(subModulesMatch[1])) !== null) {
|
|
192
|
+
packages.push(`${pkg}/${match[1].trim()}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
// Ignore
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Extract artifactId for metadata
|
|
202
|
+
const artifactIdMatch = content.match(/<artifactId>([^<]+)<\/artifactId>/);
|
|
203
|
+
const groupIdMatch = content.match(/<groupId>([^<]+)<\/groupId>/);
|
|
204
|
+
|
|
205
|
+
return [{
|
|
206
|
+
type: 'maven',
|
|
207
|
+
configFile: 'pom.xml',
|
|
208
|
+
packages: [...new Set(packages)],
|
|
209
|
+
metadata: {
|
|
210
|
+
artifactId: artifactIdMatch?.[1],
|
|
211
|
+
groupId: groupIdMatch?.[1]
|
|
212
|
+
}
|
|
213
|
+
}];
|
|
214
|
+
} catch {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
220
|
+
// Bazel
|
|
221
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
222
|
+
|
|
223
|
+
function detectBazel(projectPath) {
|
|
224
|
+
const workspaceFiles = ['WORKSPACE', 'WORKSPACE.bazel', 'MODULE.bazel'];
|
|
225
|
+
let foundConfig = null;
|
|
226
|
+
|
|
227
|
+
for (const wsFile of workspaceFiles) {
|
|
228
|
+
const wsPath = join(projectPath, wsFile);
|
|
229
|
+
if (existsSync(wsPath)) {
|
|
230
|
+
foundConfig = wsFile;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!foundConfig) return [];
|
|
236
|
+
|
|
237
|
+
// Find all BUILD files to identify packages
|
|
238
|
+
const packages = [];
|
|
239
|
+
try {
|
|
240
|
+
const buildFiles = globSync('**/BUILD{,.bazel}', {
|
|
241
|
+
cwd: projectPath,
|
|
242
|
+
ignore: ['bazel-*/**', 'node_modules/**', '.git/**']
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
for (const buildFile of buildFiles) {
|
|
246
|
+
const dir = dirname(buildFile);
|
|
247
|
+
if (dir !== '.') {
|
|
248
|
+
packages.push(dir);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
// Ignore glob errors
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return [{
|
|
256
|
+
type: 'bazel',
|
|
257
|
+
configFile: foundConfig,
|
|
258
|
+
packages,
|
|
259
|
+
metadata: { isBzlmod: foundConfig === 'MODULE.bazel' }
|
|
260
|
+
}];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
264
|
+
// Buck/Buck2 (Meta)
|
|
265
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
266
|
+
|
|
267
|
+
function detectBuck(projectPath) {
|
|
268
|
+
const buckConfigs = ['.buckconfig'];
|
|
269
|
+
let foundConfig = null;
|
|
270
|
+
|
|
271
|
+
for (const cfg of buckConfigs) {
|
|
272
|
+
if (existsSync(join(projectPath, cfg))) {
|
|
273
|
+
foundConfig = cfg;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!foundConfig) return [];
|
|
279
|
+
|
|
280
|
+
// Find all BUCK files
|
|
281
|
+
const packages = [];
|
|
282
|
+
try {
|
|
283
|
+
const buckFiles = globSync('**/BUCK{,.v2}', {
|
|
284
|
+
cwd: projectPath,
|
|
285
|
+
ignore: ['buck-out/**', 'node_modules/**', '.git/**']
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
for (const buckFile of buckFiles) {
|
|
289
|
+
const dir = dirname(buckFile);
|
|
290
|
+
if (dir !== '.') {
|
|
291
|
+
packages.push(dir);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} catch {
|
|
295
|
+
// Ignore glob errors
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return [{
|
|
299
|
+
type: 'buck',
|
|
300
|
+
configFile: foundConfig,
|
|
301
|
+
packages,
|
|
302
|
+
metadata: {}
|
|
303
|
+
}];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
307
|
+
// Pants
|
|
308
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
309
|
+
|
|
310
|
+
function detectPants(projectPath) {
|
|
311
|
+
const pantsPath = join(projectPath, 'pants.toml');
|
|
312
|
+
if (!existsSync(pantsPath)) return [];
|
|
313
|
+
|
|
314
|
+
const packages = [];
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const content = readFileSync(pantsPath, 'utf-8');
|
|
318
|
+
|
|
319
|
+
// Parse source_roots from pants.toml
|
|
320
|
+
// [source]
|
|
321
|
+
// root_patterns = ["src/*", "tests/*"]
|
|
322
|
+
const rootPatternsMatch = content.match(/root_patterns\s*=\s*\[(.*?)\]/s);
|
|
323
|
+
if (rootPatternsMatch) {
|
|
324
|
+
const patterns = rootPatternsMatch[1].match(/["']([^"']+)["']/g);
|
|
325
|
+
if (patterns) {
|
|
326
|
+
for (const p of patterns) {
|
|
327
|
+
const pattern = p.replace(/["']/g, '');
|
|
328
|
+
const baseDir = pattern.replace(/\/?\*.*$/, '');
|
|
329
|
+
if (baseDir && existsSync(join(projectPath, baseDir))) {
|
|
330
|
+
try {
|
|
331
|
+
const entries = readdirSync(join(projectPath, baseDir), { withFileTypes: true });
|
|
332
|
+
for (const entry of entries) {
|
|
333
|
+
if (entry.isDirectory()) {
|
|
334
|
+
packages.push(`${baseDir}/${entry.name}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} catch {
|
|
338
|
+
// Ignore
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
} catch {
|
|
345
|
+
// Ignore parse errors
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Also find BUILD files (Pants uses same format as Bazel)
|
|
349
|
+
try {
|
|
350
|
+
const buildFiles = globSync('**/BUILD', {
|
|
351
|
+
cwd: projectPath,
|
|
352
|
+
ignore: ['dist/**', 'node_modules/**', '.git/**', '.pants.d/**']
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
for (const buildFile of buildFiles) {
|
|
356
|
+
const dir = dirname(buildFile);
|
|
357
|
+
if (dir !== '.' && !packages.includes(dir)) {
|
|
358
|
+
packages.push(dir);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
// Ignore glob errors
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return [{
|
|
366
|
+
type: 'pants',
|
|
367
|
+
configFile: 'pants.toml',
|
|
368
|
+
packages: [...new Set(packages)],
|
|
369
|
+
metadata: {}
|
|
370
|
+
}];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
374
|
+
// Go Workspaces
|
|
375
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
376
|
+
|
|
377
|
+
function detectGoWorkspace(projectPath) {
|
|
378
|
+
const goWorkPath = join(projectPath, 'go.work');
|
|
379
|
+
if (!existsSync(goWorkPath)) return [];
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
const content = readFileSync(goWorkPath, 'utf-8');
|
|
383
|
+
const packages = [];
|
|
384
|
+
|
|
385
|
+
// Parse use directives
|
|
386
|
+
// use (
|
|
387
|
+
// ./cmd/server
|
|
388
|
+
// ./pkg/utils
|
|
389
|
+
// )
|
|
390
|
+
// or: use ./cmd/server
|
|
391
|
+
const useBlockMatch = content.match(/use\s*\(([\s\S]*?)\)/);
|
|
392
|
+
if (useBlockMatch) {
|
|
393
|
+
const lines = useBlockMatch[1].split('\n');
|
|
394
|
+
for (const line of lines) {
|
|
395
|
+
const trimmed = line.trim();
|
|
396
|
+
if (trimmed && !trimmed.startsWith('//')) {
|
|
397
|
+
packages.push(trimmed.replace(/^\.\//, ''));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Single use directive
|
|
403
|
+
const singleUsePattern = /^use\s+(\S+)/gm;
|
|
404
|
+
let match;
|
|
405
|
+
while ((match = singleUsePattern.exec(content)) !== null) {
|
|
406
|
+
if (!match[1].startsWith('(')) {
|
|
407
|
+
packages.push(match[1].replace(/^\.\//, ''));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return [{
|
|
412
|
+
type: 'go-workspace',
|
|
413
|
+
configFile: 'go.work',
|
|
414
|
+
packages: [...new Set(packages)],
|
|
415
|
+
metadata: {}
|
|
416
|
+
}];
|
|
417
|
+
} catch {
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
423
|
+
// .NET Solutions
|
|
424
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
425
|
+
|
|
426
|
+
function detectDotNet(projectPath) {
|
|
427
|
+
const results = [];
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
// Find .sln files
|
|
431
|
+
const slnFiles = globSync('*.sln', { cwd: projectPath });
|
|
432
|
+
|
|
433
|
+
for (const slnFile of slnFiles) {
|
|
434
|
+
const slnPath = join(projectPath, slnFile);
|
|
435
|
+
const content = readFileSync(slnPath, 'utf-8');
|
|
436
|
+
const packages = [];
|
|
437
|
+
|
|
438
|
+
// Parse Project lines
|
|
439
|
+
// Project("{GUID}") = "ProjectName", "path\to\project.csproj", "{GUID}"
|
|
440
|
+
const projectPattern = /Project\("[^"]+"\)\s*=\s*"([^"]+)",\s*"([^"]+)",\s*"[^"]+"/g;
|
|
441
|
+
let match;
|
|
442
|
+
while ((match = projectPattern.exec(content)) !== null) {
|
|
443
|
+
const projectPath = match[2].replace(/\\/g, '/');
|
|
444
|
+
// Get directory containing the .csproj
|
|
445
|
+
const projectDir = dirname(projectPath);
|
|
446
|
+
if (projectDir && projectDir !== '.') {
|
|
447
|
+
packages.push(projectDir);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
results.push({
|
|
452
|
+
type: 'dotnet-solution',
|
|
453
|
+
configFile: slnFile,
|
|
454
|
+
packages: [...new Set(packages)],
|
|
455
|
+
metadata: {}
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
} catch {
|
|
459
|
+
// Ignore errors
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Also detect Directory.Build.props for SDK-style projects
|
|
463
|
+
if (existsSync(join(projectPath, 'Directory.Build.props'))) {
|
|
464
|
+
// Find all .csproj files
|
|
465
|
+
try {
|
|
466
|
+
const csprojFiles = globSync('**/*.csproj', {
|
|
467
|
+
cwd: projectPath,
|
|
468
|
+
ignore: ['**/bin/**', '**/obj/**', 'node_modules/**']
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
const packages = csprojFiles.map(f => dirname(f)).filter(d => d !== '.');
|
|
472
|
+
|
|
473
|
+
if (packages.length > 0 && !results.some(r => r.type === 'dotnet-solution')) {
|
|
474
|
+
results.push({
|
|
475
|
+
type: 'dotnet-sdk',
|
|
476
|
+
configFile: 'Directory.Build.props',
|
|
477
|
+
packages: [...new Set(packages)],
|
|
478
|
+
metadata: {}
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
} catch {
|
|
482
|
+
// Ignore
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return results;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
490
|
+
// Cargo Workspaces (Rust)
|
|
491
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
492
|
+
|
|
493
|
+
function detectCargo(projectPath) {
|
|
494
|
+
const cargoPath = join(projectPath, 'Cargo.toml');
|
|
495
|
+
if (!existsSync(cargoPath)) return [];
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
const content = readFileSync(cargoPath, 'utf-8');
|
|
499
|
+
const packages = [];
|
|
500
|
+
|
|
501
|
+
// Check for [workspace] section
|
|
502
|
+
if (!content.includes('[workspace]')) {
|
|
503
|
+
// Single crate, not a workspace
|
|
504
|
+
return [{
|
|
505
|
+
type: 'cargo-single',
|
|
506
|
+
configFile: 'Cargo.toml',
|
|
507
|
+
packages: [],
|
|
508
|
+
metadata: {}
|
|
509
|
+
}];
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Parse members
|
|
513
|
+
// [workspace]
|
|
514
|
+
// members = ["crates/*", "tools/cli"]
|
|
515
|
+
const membersMatch = content.match(/members\s*=\s*\[([\s\S]*?)\]/);
|
|
516
|
+
if (membersMatch) {
|
|
517
|
+
const memberStrings = membersMatch[1].match(/["']([^"']+)["']/g);
|
|
518
|
+
if (memberStrings) {
|
|
519
|
+
for (const memberStr of memberStrings) {
|
|
520
|
+
const member = memberStr.replace(/["']/g, '');
|
|
521
|
+
if (member.includes('*')) {
|
|
522
|
+
// Glob pattern - expand it
|
|
523
|
+
const baseDir = member.replace(/\/?\*.*$/, '');
|
|
524
|
+
if (existsSync(join(projectPath, baseDir))) {
|
|
525
|
+
try {
|
|
526
|
+
const entries = readdirSync(join(projectPath, baseDir), { withFileTypes: true });
|
|
527
|
+
for (const entry of entries) {
|
|
528
|
+
if (entry.isDirectory() && existsSync(join(projectPath, baseDir, entry.name, 'Cargo.toml'))) {
|
|
529
|
+
packages.push(`${baseDir}/${entry.name}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
} catch {
|
|
533
|
+
// Ignore
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
packages.push(member);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Parse exclude (these should NOT be packages)
|
|
544
|
+
const excludeMatch = content.match(/exclude\s*=\s*\[([\s\S]*?)\]/);
|
|
545
|
+
const excludes = new Set();
|
|
546
|
+
if (excludeMatch) {
|
|
547
|
+
const excludeStrings = excludeMatch[1].match(/["']([^"']+)["']/g);
|
|
548
|
+
if (excludeStrings) {
|
|
549
|
+
for (const excStr of excludeStrings) {
|
|
550
|
+
excludes.add(excStr.replace(/["']/g, ''));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return [{
|
|
556
|
+
type: 'cargo-workspace',
|
|
557
|
+
configFile: 'Cargo.toml',
|
|
558
|
+
packages: packages.filter(p => !excludes.has(p)),
|
|
559
|
+
metadata: { excludes: [...excludes] }
|
|
560
|
+
}];
|
|
561
|
+
} catch {
|
|
562
|
+
return [];
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
567
|
+
// Python Projects
|
|
568
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
569
|
+
|
|
570
|
+
function detectPythonProject(projectPath) {
|
|
571
|
+
const results = [];
|
|
572
|
+
|
|
573
|
+
// Check pyproject.toml (PEP 518)
|
|
574
|
+
const pyprojectPath = join(projectPath, 'pyproject.toml');
|
|
575
|
+
if (existsSync(pyprojectPath)) {
|
|
576
|
+
try {
|
|
577
|
+
const content = readFileSync(pyprojectPath, 'utf-8');
|
|
578
|
+
const packages = [];
|
|
579
|
+
|
|
580
|
+
// Check for Poetry workspaces (experimental)
|
|
581
|
+
// [tool.poetry.packages]
|
|
582
|
+
// or src layout detection
|
|
583
|
+
if (existsSync(join(projectPath, 'src'))) {
|
|
584
|
+
try {
|
|
585
|
+
const srcEntries = readdirSync(join(projectPath, 'src'), { withFileTypes: true });
|
|
586
|
+
for (const entry of srcEntries) {
|
|
587
|
+
if (entry.isDirectory() && !entry.name.startsWith('_')) {
|
|
588
|
+
packages.push(`src/${entry.name}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
} catch {
|
|
592
|
+
// Ignore
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Check for package names in [tool.poetry] or [project]
|
|
597
|
+
const nameMatch = content.match(/name\s*=\s*["']([^"']+)["']/);
|
|
598
|
+
|
|
599
|
+
results.push({
|
|
600
|
+
type: 'python-pyproject',
|
|
601
|
+
configFile: 'pyproject.toml',
|
|
602
|
+
packages,
|
|
603
|
+
metadata: { name: nameMatch?.[1] }
|
|
604
|
+
});
|
|
605
|
+
} catch {
|
|
606
|
+
// Ignore
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Check setup.py
|
|
611
|
+
const setupPyPath = join(projectPath, 'setup.py');
|
|
612
|
+
if (existsSync(setupPyPath)) {
|
|
613
|
+
results.push({
|
|
614
|
+
type: 'python-setup',
|
|
615
|
+
configFile: 'setup.py',
|
|
616
|
+
packages: existsSync(join(projectPath, 'src')) ? ['src'] : [],
|
|
617
|
+
metadata: {}
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return results;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Merge packages from all build systems into the monorepo detection
|
|
626
|
+
* This is called from deadcode.mjs extractPathAliases
|
|
627
|
+
* @param {string} projectPath - Path to project root
|
|
628
|
+
* @returns {Array<{dir: string, prefix: string}>} - Config dirs for alias extraction
|
|
629
|
+
*/
|
|
630
|
+
export function getConfigDirsFromBuildSystems(projectPath) {
|
|
631
|
+
const systems = detectBuildSystems(projectPath);
|
|
632
|
+
const configDirs = [];
|
|
633
|
+
|
|
634
|
+
for (const system of systems) {
|
|
635
|
+
for (const pkg of system.packages || []) {
|
|
636
|
+
configDirs.push({ dir: pkg, prefix: `${pkg}/` });
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return configDirs;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export default {
|
|
644
|
+
detectBuildSystems,
|
|
645
|
+
getPackageDirectories,
|
|
646
|
+
getConfigDirsFromBuildSystems
|
|
647
|
+
};
|