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.
Files changed (36) hide show
  1. package/README.md +113 -0
  2. package/bin/swynx-lite +3 -0
  3. package/package.json +47 -0
  4. package/src/clean.mjs +280 -0
  5. package/src/cli.mjs +264 -0
  6. package/src/config.mjs +121 -0
  7. package/src/output/console.mjs +298 -0
  8. package/src/output/json.mjs +76 -0
  9. package/src/output/progress.mjs +57 -0
  10. package/src/scan.mjs +143 -0
  11. package/src/security.mjs +62 -0
  12. package/src/shared/fixer/barrel-cleaner.mjs +192 -0
  13. package/src/shared/fixer/import-cleaner.mjs +237 -0
  14. package/src/shared/fixer/quarantine.mjs +218 -0
  15. package/src/shared/scanner/analysers/buildSystems.mjs +647 -0
  16. package/src/shared/scanner/analysers/configParsers.mjs +1086 -0
  17. package/src/shared/scanner/analysers/deadcode.mjs +6194 -0
  18. package/src/shared/scanner/analysers/entryPointDetector.mjs +634 -0
  19. package/src/shared/scanner/analysers/generatedCode.mjs +297 -0
  20. package/src/shared/scanner/analysers/imports.mjs +60 -0
  21. package/src/shared/scanner/discovery.mjs +240 -0
  22. package/src/shared/scanner/parse-worker.mjs +82 -0
  23. package/src/shared/scanner/parsers/assets.mjs +44 -0
  24. package/src/shared/scanner/parsers/csharp.mjs +400 -0
  25. package/src/shared/scanner/parsers/css.mjs +60 -0
  26. package/src/shared/scanner/parsers/go.mjs +445 -0
  27. package/src/shared/scanner/parsers/java.mjs +364 -0
  28. package/src/shared/scanner/parsers/javascript.mjs +823 -0
  29. package/src/shared/scanner/parsers/kotlin.mjs +350 -0
  30. package/src/shared/scanner/parsers/python.mjs +497 -0
  31. package/src/shared/scanner/parsers/registry.mjs +233 -0
  32. package/src/shared/scanner/parsers/rust.mjs +427 -0
  33. package/src/shared/scanner/scan-dead-code.mjs +316 -0
  34. package/src/shared/security/patterns.mjs +349 -0
  35. package/src/shared/security/proximity.mjs +84 -0
  36. package/src/shared/security/scanner.mjs +269 -0
@@ -0,0 +1,1086 @@
1
+ // src/scanner/analysers/configParsers.mjs
2
+ // CI/CD and Bundler configuration parsers for entry point detection
3
+
4
+ import { readFileSync, existsSync } from 'fs';
5
+ import { join, dirname, basename, relative } from 'path';
6
+ import { globSync } from 'glob';
7
+
8
+ /**
9
+ * Parse Webpack configuration for entry points
10
+ * @param {string} projectPath - Project root path
11
+ * @returns {Object} - { entries: string[], mode: string }
12
+ */
13
+ export function parseWebpackConfig(projectPath) {
14
+ const configFiles = [
15
+ 'webpack.config.js',
16
+ 'webpack.config.mjs',
17
+ 'webpack.config.ts',
18
+ 'webpack.config.cjs',
19
+ 'webpack.dev.js',
20
+ 'webpack.prod.js',
21
+ 'webpack.common.js'
22
+ ];
23
+
24
+ const entries = [];
25
+ let mode = 'unknown';
26
+
27
+ for (const configFile of configFiles) {
28
+ const configPath = join(projectPath, configFile);
29
+ if (!existsSync(configPath)) continue;
30
+
31
+ try {
32
+ const content = readFileSync(configPath, 'utf-8');
33
+
34
+ // Extract entry points
35
+ // Pattern: entry: './src/index.js' or entry: { main: './src/index.js' }
36
+ const singleEntryMatch = content.match(/entry\s*:\s*['"]([^'"]+)['"]/);
37
+ if (singleEntryMatch) {
38
+ entries.push(singleEntryMatch[1]);
39
+ }
40
+
41
+ // Object entries: entry: { name: 'path' }
42
+ const objectEntryMatch = content.match(/entry\s*:\s*\{([^}]+)\}/s);
43
+ if (objectEntryMatch) {
44
+ const entryBlock = objectEntryMatch[1];
45
+ const pathMatches = entryBlock.matchAll(/['"]([^'"]+\.(?:js|ts|jsx|tsx|mjs))['"]/g);
46
+ for (const match of pathMatches) {
47
+ entries.push(match[1]);
48
+ }
49
+ }
50
+
51
+ // Array entries: entry: ['./src/a.js', './src/b.js']
52
+ const arrayEntryMatch = content.match(/entry\s*:\s*\[([^\]]+)\]/s);
53
+ if (arrayEntryMatch) {
54
+ const arrayBlock = arrayEntryMatch[1];
55
+ const pathMatches = arrayBlock.matchAll(/['"]([^'"]+)['"]/g);
56
+ for (const match of pathMatches) {
57
+ entries.push(match[1]);
58
+ }
59
+ }
60
+
61
+ // Detect mode
62
+ if (content.includes("mode: 'production'") || content.includes('mode: "production"')) {
63
+ mode = 'production';
64
+ } else if (content.includes("mode: 'development'") || content.includes('mode: "development"')) {
65
+ mode = 'development';
66
+ }
67
+ } catch {
68
+ // Ignore parse errors
69
+ }
70
+ }
71
+
72
+ return { entries: [...new Set(entries)], mode };
73
+ }
74
+
75
+ /**
76
+ * Parse Vite configuration for entry points
77
+ * @param {string} projectPath - Project root path
78
+ * @returns {Object} - { entries: string[], framework: string|null }
79
+ */
80
+ export function parseViteConfig(projectPath) {
81
+ const configFiles = [
82
+ 'vite.config.js',
83
+ 'vite.config.ts',
84
+ 'vite.config.mjs'
85
+ ];
86
+
87
+ const entries = [];
88
+ let framework = null;
89
+
90
+ for (const configFile of configFiles) {
91
+ const configPath = join(projectPath, configFile);
92
+ if (!existsSync(configPath)) continue;
93
+
94
+ try {
95
+ const content = readFileSync(configPath, 'utf-8');
96
+
97
+ // Default entry is index.html, but check for custom entries
98
+ // build.rollupOptions.input
99
+ const inputMatch = content.match(/input\s*:\s*['"]([^'"]+)['"]/);
100
+ if (inputMatch) {
101
+ entries.push(inputMatch[1]);
102
+ }
103
+
104
+ // Object input: { main: 'src/main.ts' }
105
+ const objectInputMatch = content.match(/input\s*:\s*\{([^}]+)\}/s);
106
+ if (objectInputMatch) {
107
+ const inputBlock = objectInputMatch[1];
108
+ const pathMatches = inputBlock.matchAll(/['"]([^'"]+\.(?:html|js|ts|jsx|tsx))['"]/g);
109
+ for (const match of pathMatches) {
110
+ entries.push(match[1]);
111
+ }
112
+ }
113
+
114
+ // Detect framework
115
+ if (content.includes('@vitejs/plugin-react') || content.includes('vite-plugin-react')) {
116
+ framework = 'react';
117
+ } else if (content.includes('@vitejs/plugin-vue')) {
118
+ framework = 'vue';
119
+ } else if (content.includes('@sveltejs/vite-plugin-svelte')) {
120
+ framework = 'svelte';
121
+ }
122
+ } catch {
123
+ // Ignore parse errors
124
+ }
125
+ }
126
+
127
+ // Check for index.html as default entry
128
+ if (entries.length === 0 && existsSync(join(projectPath, 'index.html'))) {
129
+ entries.push('index.html');
130
+ }
131
+
132
+ return { entries: [...new Set(entries)], framework };
133
+ }
134
+
135
+ /**
136
+ * Parse Rollup configuration for entry points
137
+ * @param {string} projectPath - Project root path
138
+ * @returns {Object} - { entries: string[], outputFormats: string[] }
139
+ */
140
+ export function parseRollupConfig(projectPath) {
141
+ const configFiles = [
142
+ 'rollup.config.js',
143
+ 'rollup.config.mjs',
144
+ 'rollup.config.ts'
145
+ ];
146
+
147
+ const entries = [];
148
+ const outputFormats = [];
149
+
150
+ for (const configFile of configFiles) {
151
+ const configPath = join(projectPath, configFile);
152
+ if (!existsSync(configPath)) continue;
153
+
154
+ try {
155
+ const content = readFileSync(configPath, 'utf-8');
156
+
157
+ // Input: 'src/index.js' or input: ['src/a.js', 'src/b.js']
158
+ const singleInputMatch = content.match(/input\s*:\s*['"]([^'"]+)['"]/);
159
+ if (singleInputMatch) {
160
+ entries.push(singleInputMatch[1]);
161
+ }
162
+
163
+ const arrayInputMatch = content.match(/input\s*:\s*\[([^\]]+)\]/s);
164
+ if (arrayInputMatch) {
165
+ const arrayBlock = arrayInputMatch[1];
166
+ const pathMatches = arrayBlock.matchAll(/['"]([^'"]+)['"]/g);
167
+ for (const match of pathMatches) {
168
+ entries.push(match[1]);
169
+ }
170
+ }
171
+
172
+ // Detect output formats
173
+ const formatMatches = content.matchAll(/format\s*:\s*['"](\w+)['"]/g);
174
+ for (const match of formatMatches) {
175
+ outputFormats.push(match[1]);
176
+ }
177
+ } catch {
178
+ // Ignore parse errors
179
+ }
180
+ }
181
+
182
+ return { entries: [...new Set(entries)], outputFormats: [...new Set(outputFormats)] };
183
+ }
184
+
185
+ /**
186
+ * Parse esbuild configuration for entry points
187
+ * @param {string} projectPath - Project root path
188
+ * @returns {Object} - { entries: string[] }
189
+ */
190
+ export function parseEsbuildConfig(projectPath) {
191
+ const configFiles = [
192
+ 'esbuild.config.js',
193
+ 'esbuild.config.mjs',
194
+ 'esbuild.mjs',
195
+ 'build.mjs'
196
+ ];
197
+
198
+ const entries = [];
199
+
200
+ for (const configFile of configFiles) {
201
+ const configPath = join(projectPath, configFile);
202
+ if (!existsSync(configPath)) continue;
203
+
204
+ try {
205
+ const content = readFileSync(configPath, 'utf-8');
206
+
207
+ // entryPoints: ['src/index.ts']
208
+ const entryPointsMatch = content.match(/entryPoints\s*:\s*\[([^\]]+)\]/s);
209
+ if (entryPointsMatch) {
210
+ const arrayBlock = entryPointsMatch[1];
211
+ const pathMatches = arrayBlock.matchAll(/['"]([^'"]+)['"]/g);
212
+ for (const match of pathMatches) {
213
+ entries.push(match[1]);
214
+ }
215
+ }
216
+ } catch {
217
+ // Ignore parse errors
218
+ }
219
+ }
220
+
221
+ return { entries: [...new Set(entries)] };
222
+ }
223
+
224
+ /**
225
+ * Parse Parcel configuration (uses package.json source/main)
226
+ * @param {string} projectPath - Project root path
227
+ * @returns {Object} - { entries: string[] }
228
+ */
229
+ export function parseParcelConfig(projectPath) {
230
+ const entries = [];
231
+
232
+ const pkgPath = join(projectPath, 'package.json');
233
+ if (existsSync(pkgPath)) {
234
+ try {
235
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
236
+
237
+ // Parcel uses 'source' field
238
+ if (pkg.source) {
239
+ if (Array.isArray(pkg.source)) {
240
+ entries.push(...pkg.source);
241
+ } else {
242
+ entries.push(pkg.source);
243
+ }
244
+ }
245
+
246
+ // Also check for targets in .parcelrc
247
+ const parcelrcPath = join(projectPath, '.parcelrc');
248
+ if (existsSync(parcelrcPath)) {
249
+ const parcelrc = JSON.parse(readFileSync(parcelrcPath, 'utf-8'));
250
+ // Extract entries from targets if defined
251
+ }
252
+ } catch {
253
+ // Ignore parse errors
254
+ }
255
+ }
256
+
257
+ return { entries: [...new Set(entries)] };
258
+ }
259
+
260
+ /**
261
+ * Parse GitHub Actions workflow for script references
262
+ * @param {string} projectPath - Project root path
263
+ * @returns {Object} - { scripts: string[], testCommands: string[] }
264
+ */
265
+ export function parseGitHubActions(projectPath) {
266
+ const workflowDir = join(projectPath, '.github', 'workflows');
267
+ if (!existsSync(workflowDir)) {
268
+ return { scripts: [], testCommands: [] };
269
+ }
270
+
271
+ const scripts = [];
272
+ const testCommands = [];
273
+
274
+ try {
275
+ const workflowFiles = globSync('*.{yml,yaml}', { cwd: workflowDir });
276
+
277
+ for (const file of workflowFiles) {
278
+ const content = readFileSync(join(workflowDir, file), 'utf-8');
279
+
280
+ // Extract run commands
281
+ const runMatches = content.matchAll(/run\s*:\s*(?:\|-)?\s*\n?\s*(.+)/g);
282
+ for (const match of runMatches) {
283
+ const command = match[1].trim();
284
+
285
+ // Look for script executions
286
+ const scriptMatch = command.match(/(?:node|npx|ts-node|tsx)\s+([^\s|&;]+)/);
287
+ if (scriptMatch) {
288
+ scripts.push(scriptMatch[1]);
289
+ }
290
+
291
+ // Look for test commands
292
+ if (command.includes('test') || command.includes('jest') || command.includes('vitest') ||
293
+ command.includes('mocha') || command.includes('cypress') || command.includes('playwright')) {
294
+ testCommands.push(command);
295
+ }
296
+ }
297
+
298
+ // Extract npm/yarn script references
299
+ const npmRunMatches = content.matchAll(/(?:npm|yarn|pnpm)\s+(?:run\s+)?(\w+)/g);
300
+ for (const match of npmRunMatches) {
301
+ scripts.push(`npm:${match[1]}`);
302
+ }
303
+ }
304
+ } catch {
305
+ // Ignore errors
306
+ }
307
+
308
+ return { scripts: [...new Set(scripts)], testCommands: [...new Set(testCommands)] };
309
+ }
310
+
311
+ /**
312
+ * Parse GitLab CI configuration
313
+ * @param {string} projectPath - Project root path
314
+ * @returns {Object} - { scripts: string[], stages: string[] }
315
+ */
316
+ export function parseGitLabCI(projectPath) {
317
+ const ciPath = join(projectPath, '.gitlab-ci.yml');
318
+ if (!existsSync(ciPath)) {
319
+ return { scripts: [], stages: [] };
320
+ }
321
+
322
+ const scripts = [];
323
+ const stages = [];
324
+
325
+ try {
326
+ const content = readFileSync(ciPath, 'utf-8');
327
+
328
+ // Extract stages
329
+ const stagesMatch = content.match(/stages:\s*\n((?:\s+-\s+\w+\n?)*)/);
330
+ if (stagesMatch) {
331
+ const stageLines = stagesMatch[1].split('\n');
332
+ for (const line of stageLines) {
333
+ const match = line.match(/^\s*-\s+(\w+)/);
334
+ if (match) stages.push(match[1]);
335
+ }
336
+ }
337
+
338
+ // Extract script commands
339
+ const scriptMatches = content.matchAll(/script:\s*\n?((?:\s+-\s+.+\n?)*)/g);
340
+ for (const match of scriptMatches) {
341
+ const scriptLines = match[1].split('\n');
342
+ for (const line of scriptLines) {
343
+ const cmdMatch = line.match(/^\s*-\s+(.+)/);
344
+ if (cmdMatch) {
345
+ const command = cmdMatch[1].trim();
346
+ const scriptMatch = command.match(/(?:node|npx|ts-node|tsx)\s+([^\s|&;]+)/);
347
+ if (scriptMatch) {
348
+ scripts.push(scriptMatch[1]);
349
+ }
350
+ }
351
+ }
352
+ }
353
+ } catch {
354
+ // Ignore errors
355
+ }
356
+
357
+ return { scripts: [...new Set(scripts)], stages: [...new Set(stages)] };
358
+ }
359
+
360
+ /**
361
+ * Parse Jenkins configuration (Jenkinsfile)
362
+ * @param {string} projectPath - Project root path
363
+ * @returns {Object} - { scripts: string[], stages: string[] }
364
+ */
365
+ export function parseJenkinsfile(projectPath) {
366
+ const jenkinsfiles = ['Jenkinsfile', 'jenkinsfile', 'Jenkinsfile.groovy'];
367
+ const scripts = [];
368
+ const stages = [];
369
+
370
+ for (const jenkinsfile of jenkinsfiles) {
371
+ const filePath = join(projectPath, jenkinsfile);
372
+ if (!existsSync(filePath)) continue;
373
+
374
+ try {
375
+ const content = readFileSync(filePath, 'utf-8');
376
+
377
+ // Extract stage names
378
+ const stageMatches = content.matchAll(/stage\s*\(\s*['"]([^'"]+)['"]\s*\)/g);
379
+ for (const match of stageMatches) {
380
+ stages.push(match[1]);
381
+ }
382
+
383
+ // Extract sh commands
384
+ const shMatches = content.matchAll(/sh\s+['"]([^'"]+)['"]/g);
385
+ for (const match of shMatches) {
386
+ const command = match[1];
387
+ const scriptMatch = command.match(/(?:node|npx|ts-node|tsx)\s+([^\s|&;]+)/);
388
+ if (scriptMatch) {
389
+ scripts.push(scriptMatch[1]);
390
+ }
391
+ }
392
+ } catch {
393
+ // Ignore errors
394
+ }
395
+ }
396
+
397
+ return { scripts: [...new Set(scripts)], stages: [...new Set(stages)] };
398
+ }
399
+
400
+ /**
401
+ * Parse Docker configuration for entry points
402
+ * @param {string} projectPath - Project root path
403
+ * @returns {Object} - { entrypoints: string[], cmdScripts: string[] }
404
+ */
405
+ export function parseDockerConfig(projectPath) {
406
+ const dockerfiles = ['Dockerfile', 'dockerfile', 'Dockerfile.dev', 'Dockerfile.prod'];
407
+ const entrypoints = [];
408
+ const cmdScripts = [];
409
+
410
+ for (const dockerfile of dockerfiles) {
411
+ const filePath = join(projectPath, dockerfile);
412
+ if (!existsSync(filePath)) continue;
413
+
414
+ try {
415
+ const content = readFileSync(filePath, 'utf-8');
416
+
417
+ // Extract ENTRYPOINT
418
+ const entrypointMatches = content.matchAll(/ENTRYPOINT\s+\[([^\]]+)\]/g);
419
+ for (const match of entrypointMatches) {
420
+ const parts = match[1].match(/['"]([^'"]+)['"]/g);
421
+ if (parts) {
422
+ const script = parts.find(p => p.includes('.js') || p.includes('.ts') || p.includes('.mjs'));
423
+ if (script) entrypoints.push(script.replace(/['"]/g, ''));
424
+ }
425
+ }
426
+
427
+ // Extract CMD
428
+ const cmdMatches = content.matchAll(/CMD\s+\[([^\]]+)\]/g);
429
+ for (const match of cmdMatches) {
430
+ const parts = match[1].match(/['"]([^'"]+)['"]/g);
431
+ if (parts) {
432
+ const script = parts.find(p => p.includes('.js') || p.includes('.ts') || p.includes('.mjs'));
433
+ if (script) cmdScripts.push(script.replace(/['"]/g, ''));
434
+ }
435
+ }
436
+
437
+ // Shell form: CMD node app.js
438
+ const shellCmdMatch = content.match(/CMD\s+(?:node|npm|yarn|npx)\s+([^\s\n]+)/);
439
+ if (shellCmdMatch) {
440
+ cmdScripts.push(shellCmdMatch[1]);
441
+ }
442
+ } catch {
443
+ // Ignore errors
444
+ }
445
+ }
446
+
447
+ // Check docker-compose.yml
448
+ const composeFiles = ['docker-compose.yml', 'docker-compose.yaml', 'compose.yml', 'compose.yaml'];
449
+ for (const composeFile of composeFiles) {
450
+ const filePath = join(projectPath, composeFile);
451
+ if (!existsSync(filePath)) continue;
452
+
453
+ try {
454
+ const content = readFileSync(filePath, 'utf-8');
455
+
456
+ // Extract command from services
457
+ const commandMatches = content.matchAll(/command:\s*(?:\[([^\]]+)\]|(.+))/g);
458
+ for (const match of commandMatches) {
459
+ const cmdBlock = match[1] || match[2];
460
+ const scriptMatch = cmdBlock?.match(/(?:node|npm|yarn|npx)\s+([^\s|&;'"]+)/);
461
+ if (scriptMatch) {
462
+ cmdScripts.push(scriptMatch[1]);
463
+ }
464
+ }
465
+ } catch {
466
+ // Ignore errors
467
+ }
468
+ }
469
+
470
+ return {
471
+ entrypoints: [...new Set(entrypoints)],
472
+ cmdScripts: [...new Set(cmdScripts)]
473
+ };
474
+ }
475
+
476
+ /**
477
+ * Parse Webpack Module Federation exposes configuration
478
+ * Searches root and common subdirectories for monorepo-style projects
479
+ * @param {string} projectPath - Project root path
480
+ * @returns {Object} - { exposes: string[], remotes: string[] }
481
+ */
482
+ export function parseModuleFederationConfig(projectPath) {
483
+ const configFileNames = [
484
+ 'webpack.config.js',
485
+ 'webpack.config.mjs',
486
+ 'webpack.config.ts',
487
+ 'webpack.dev.js',
488
+ 'webpack.prod.js'
489
+ ];
490
+
491
+ const exposes = [];
492
+ const remotes = [];
493
+
494
+ // Search in root and subdirectories
495
+ const searchDirs = [''];
496
+
497
+ // Find potential app directories
498
+ try {
499
+ const entries = globSync('*/', { cwd: projectPath, ignore: ['node_modules/'] });
500
+ for (const entry of entries) {
501
+ const entryPath = entry.replace(/\/$/, '');
502
+ // Check if this directory has a webpack config
503
+ for (const configName of configFileNames) {
504
+ if (existsSync(join(projectPath, entryPath, configName))) {
505
+ searchDirs.push(entryPath);
506
+ break;
507
+ }
508
+ }
509
+ }
510
+ } catch {
511
+ // Ignore glob errors
512
+ }
513
+
514
+ for (const searchDir of searchDirs) {
515
+ const basePath = searchDir ? join(projectPath, searchDir) : projectPath;
516
+ const relativePrefix = searchDir ? searchDir + '/' : '';
517
+
518
+ for (const configFile of configFileNames) {
519
+ const configPath = join(basePath, configFile);
520
+ if (!existsSync(configPath)) continue;
521
+
522
+ try {
523
+ const content = readFileSync(configPath, 'utf-8');
524
+
525
+ // Check for ModuleFederationPlugin
526
+ if (!content.includes('ModuleFederationPlugin')) continue;
527
+
528
+ // Extract entry point (add as entry)
529
+ const entryMatch = content.match(/entry\s*:\s*['"]([^'"]+)['"]/);
530
+ if (entryMatch) {
531
+ exposes.push(relativePrefix + entryMatch[1].replace(/^\.\//, ''));
532
+ }
533
+
534
+ // Extract exposes paths
535
+ // exposes: { './Button': './src/components/Button' }
536
+ const exposesMatch = content.match(/exposes\s*:\s*\{([^}]+)\}/s);
537
+ if (exposesMatch) {
538
+ const exposesBlock = exposesMatch[1];
539
+ // Match: './key': './src/path' or './key': 'src/path'
540
+ const pathMatches = exposesBlock.matchAll(/['"][^'"]+['"]\s*:\s*['"]\.?\/?(src\/[^'"]+|[^'"\/][^'"]+)['"]/g);
541
+ for (const match of pathMatches) {
542
+ const exposePath = match[1].replace(/^\.\//, '');
543
+ // Add with the relative prefix for monorepo support
544
+ exposes.push(relativePrefix + exposePath);
545
+ // Also add common extensions
546
+ exposes.push(relativePrefix + exposePath + '.js');
547
+ exposes.push(relativePrefix + exposePath + '.jsx');
548
+ exposes.push(relativePrefix + exposePath + '.ts');
549
+ exposes.push(relativePrefix + exposePath + '.tsx');
550
+ }
551
+ }
552
+
553
+ // Extract remotes for reference
554
+ const remotesMatch = content.match(/remotes\s*:\s*\{([^}]+)\}/s);
555
+ if (remotesMatch) {
556
+ const remotesBlock = remotesMatch[1];
557
+ const nameMatches = remotesBlock.matchAll(/['"](\w+)['"]\s*:/g);
558
+ for (const match of nameMatches) {
559
+ remotes.push(match[1]);
560
+ }
561
+ }
562
+ } catch {
563
+ // Ignore parse errors
564
+ }
565
+ }
566
+ }
567
+
568
+ return { exposes: [...new Set(exposes)], remotes: [...new Set(remotes)] };
569
+ }
570
+
571
+ /**
572
+ * Parse Serverless Framework configuration for handler entry points
573
+ * @param {string} projectPath - Project root path
574
+ * @returns {Object} - { handlers: string[] }
575
+ */
576
+ export function parseServerlessConfig(projectPath) {
577
+ const configFiles = [
578
+ 'serverless.yml',
579
+ 'serverless.yaml',
580
+ 'serverless.ts',
581
+ 'serverless.js'
582
+ ];
583
+
584
+ const handlers = [];
585
+
586
+ for (const configFile of configFiles) {
587
+ const configPath = join(projectPath, configFile);
588
+ if (!existsSync(configPath)) continue;
589
+
590
+ try {
591
+ const content = readFileSync(configPath, 'utf-8');
592
+
593
+ // Match handler patterns like: handler: src/handlers/hello.handler
594
+ const handlerMatches = content.matchAll(/handler\s*:\s*['"]?([^\s'"#\n]+)['"]?/g);
595
+ for (const match of handlerMatches) {
596
+ const handlerPath = match[1].trim();
597
+ // Handler format: path/to/file.functionName - extract file path
598
+ const filePath = handlerPath.replace(/\.[^.]+$/, ''); // Remove .handler suffix
599
+ // Add common extensions
600
+ handlers.push(filePath + '.js');
601
+ handlers.push(filePath + '.ts');
602
+ handlers.push(filePath + '.mjs');
603
+ }
604
+ } catch {
605
+ // Ignore parse errors
606
+ }
607
+ }
608
+
609
+ return { handlers: [...new Set(handlers)] };
610
+ }
611
+
612
+ /**
613
+ * Parse Next.js configuration and detect page/app router entry points
614
+ * @param {string} projectPath - Project root path
615
+ * @returns {Object} - { pages: string[], appRoutes: string[], apiRoutes: string[] }
616
+ */
617
+ export function parseNextjsConfig(projectPath) {
618
+ const pages = [];
619
+ const appRoutes = [];
620
+ const apiRoutes = [];
621
+
622
+ // Check for Next.js indicators
623
+ const nextConfigFiles = ['next.config.js', 'next.config.mjs', 'next.config.ts'];
624
+ let isNextProject = false;
625
+ for (const configFile of nextConfigFiles) {
626
+ if (existsSync(join(projectPath, configFile))) {
627
+ isNextProject = true;
628
+ break;
629
+ }
630
+ }
631
+
632
+ // Also check package.json for next dependency
633
+ const pkgPath = join(projectPath, 'package.json');
634
+ if (existsSync(pkgPath)) {
635
+ try {
636
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
637
+ if (pkg.dependencies?.next || pkg.devDependencies?.next) {
638
+ isNextProject = true;
639
+ }
640
+ } catch {}
641
+ }
642
+
643
+ if (!isNextProject) return { pages, appRoutes, apiRoutes };
644
+
645
+ // Scan for pages directory (Pages Router)
646
+ const pagesDirs = ['pages', 'src/pages'];
647
+ for (const pagesDir of pagesDirs) {
648
+ const fullDir = join(projectPath, pagesDir);
649
+ if (existsSync(fullDir)) {
650
+ try {
651
+ const pageFiles = globSync('**/*.{js,jsx,ts,tsx}', { cwd: fullDir, nodir: true });
652
+ for (const file of pageFiles) {
653
+ if (file.startsWith('api/')) {
654
+ apiRoutes.push(join(pagesDir, file));
655
+ } else {
656
+ pages.push(join(pagesDir, file));
657
+ }
658
+ }
659
+ } catch {}
660
+ }
661
+ }
662
+
663
+ // Scan for app directory (App Router)
664
+ const appDirs = ['app', 'src/app'];
665
+ for (const appDir of appDirs) {
666
+ const fullDir = join(projectPath, appDir);
667
+ if (existsSync(fullDir)) {
668
+ try {
669
+ // App router files: page.tsx, layout.tsx, route.ts, loading.tsx, error.tsx, etc.
670
+ const appFiles = globSync('**/{page,layout,route,loading,error,not-found,template}.{js,jsx,ts,tsx}', { cwd: fullDir, nodir: true });
671
+ for (const file of appFiles) {
672
+ if (file.includes('/api/') || file.startsWith('api/')) {
673
+ apiRoutes.push(join(appDir, file));
674
+ } else {
675
+ appRoutes.push(join(appDir, file));
676
+ }
677
+ }
678
+ } catch {}
679
+ }
680
+ }
681
+
682
+ return { pages, appRoutes, apiRoutes };
683
+ }
684
+
685
+ /**
686
+ * Parse Cypress configuration for spec and support files
687
+ * @param {string} projectPath - Project root path
688
+ * @returns {Object} - { specFiles: string[], supportFiles: string[] }
689
+ */
690
+ export function parseCypressConfig(projectPath) {
691
+ const configFiles = [
692
+ 'cypress.config.js',
693
+ 'cypress.config.ts',
694
+ 'cypress.config.mjs',
695
+ 'cypress.json' // Legacy config
696
+ ];
697
+
698
+ const specFiles = [];
699
+ const supportFiles = [];
700
+
701
+ for (const configFile of configFiles) {
702
+ const configPath = join(projectPath, configFile);
703
+ if (!existsSync(configPath)) continue;
704
+
705
+ try {
706
+ const content = readFileSync(configPath, 'utf-8');
707
+
708
+ // Extract specPattern
709
+ const specPatternMatch = content.match(/specPattern\s*:\s*['"]([^'"]+)['"]/);
710
+ if (specPatternMatch) {
711
+ const pattern = specPatternMatch[1];
712
+ // Resolve glob pattern to actual files
713
+ try {
714
+ const files = globSync(pattern, { cwd: projectPath, nodir: true });
715
+ specFiles.push(...files);
716
+ } catch {}
717
+ }
718
+
719
+ // Extract supportFile
720
+ const supportFileMatch = content.match(/supportFile\s*:\s*['"]([^'"]+)['"]/);
721
+ if (supportFileMatch) {
722
+ supportFiles.push(supportFileMatch[1]);
723
+ }
724
+
725
+ // Legacy cypress.json format
726
+ if (configFile === 'cypress.json') {
727
+ try {
728
+ const config = JSON.parse(content);
729
+ if (config.integrationFolder || config.testFiles) {
730
+ const folder = config.integrationFolder || 'cypress/integration';
731
+ const pattern = config.testFiles || '**/*.*';
732
+ const files = globSync(`${folder}/${pattern}`, { cwd: projectPath, nodir: true });
733
+ specFiles.push(...files);
734
+ }
735
+ if (config.supportFile) {
736
+ supportFiles.push(config.supportFile);
737
+ }
738
+ } catch {}
739
+ }
740
+ } catch {
741
+ // Ignore parse errors
742
+ }
743
+ }
744
+
745
+ // Default patterns if config not found but cypress folder exists
746
+ if (specFiles.length === 0 && existsSync(join(projectPath, 'cypress'))) {
747
+ try {
748
+ const defaultSpecs = globSync('cypress/e2e/**/*.cy.{js,ts,jsx,tsx}', { cwd: projectPath, nodir: true });
749
+ specFiles.push(...defaultSpecs);
750
+ // Also check legacy integration folder
751
+ const legacySpecs = globSync('cypress/integration/**/*.{js,ts,jsx,tsx}', { cwd: projectPath, nodir: true });
752
+ specFiles.push(...legacySpecs);
753
+ } catch {}
754
+ }
755
+
756
+ if (supportFiles.length === 0 && existsSync(join(projectPath, 'cypress/support'))) {
757
+ // Default support file location
758
+ if (existsSync(join(projectPath, 'cypress/support/e2e.ts'))) {
759
+ supportFiles.push('cypress/support/e2e.ts');
760
+ } else if (existsSync(join(projectPath, 'cypress/support/e2e.js'))) {
761
+ supportFiles.push('cypress/support/e2e.js');
762
+ } else if (existsSync(join(projectPath, 'cypress/support/index.ts'))) {
763
+ supportFiles.push('cypress/support/index.ts');
764
+ } else if (existsSync(join(projectPath, 'cypress/support/index.js'))) {
765
+ supportFiles.push('cypress/support/index.js');
766
+ }
767
+ }
768
+
769
+ return { specFiles: [...new Set(specFiles)], supportFiles: [...new Set(supportFiles)] };
770
+ }
771
+
772
+ /**
773
+ * Parse Jest configuration for test patterns and setup files
774
+ * @param {string} projectPath - Project root path
775
+ * @returns {Object} - { testFiles: string[], setupFiles: string[] }
776
+ */
777
+ export function parseJestConfig(projectPath) {
778
+ const configFiles = [
779
+ 'jest.config.js',
780
+ 'jest.config.ts',
781
+ 'jest.config.mjs',
782
+ 'jest.config.json'
783
+ ];
784
+
785
+ const testFiles = [];
786
+ const setupFiles = [];
787
+
788
+ // Check package.json jest config
789
+ const pkgPath = join(projectPath, 'package.json');
790
+ if (existsSync(pkgPath)) {
791
+ try {
792
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
793
+ if (pkg.jest) {
794
+ if (pkg.jest.setupFilesAfterEnv) {
795
+ setupFiles.push(...pkg.jest.setupFilesAfterEnv.map(f => f.replace(/^<rootDir>\//, '')));
796
+ }
797
+ if (pkg.jest.setupFiles) {
798
+ setupFiles.push(...pkg.jest.setupFiles.map(f => f.replace(/^<rootDir>\//, '')));
799
+ }
800
+ }
801
+ } catch {}
802
+ }
803
+
804
+ for (const configFile of configFiles) {
805
+ const configPath = join(projectPath, configFile);
806
+ if (!existsSync(configPath)) continue;
807
+
808
+ try {
809
+ const content = readFileSync(configPath, 'utf-8');
810
+
811
+ // Extract setupFilesAfterEnv
812
+ const setupMatch = content.match(/setupFilesAfterEnv\s*:\s*\[([^\]]+)\]/s);
813
+ if (setupMatch) {
814
+ const files = setupMatch[1].matchAll(/['"]([^'"]+)['"]/g);
815
+ for (const match of files) {
816
+ setupFiles.push(match[1].replace(/^<rootDir>\//, ''));
817
+ }
818
+ }
819
+
820
+ // Extract testMatch patterns
821
+ const testMatchMatch = content.match(/testMatch\s*:\s*\[([^\]]+)\]/s);
822
+ if (testMatchMatch) {
823
+ const patterns = testMatchMatch[1].matchAll(/['"]([^'"]+)['"]/g);
824
+ for (const match of patterns) {
825
+ const pattern = match[1].replace(/^<rootDir>\//, '').replace(/\*\*\//, '');
826
+ try {
827
+ const files = globSync(pattern, { cwd: projectPath, nodir: true });
828
+ testFiles.push(...files);
829
+ } catch {}
830
+ }
831
+ }
832
+ } catch {
833
+ // Ignore parse errors
834
+ }
835
+ }
836
+
837
+ // Default test patterns if none found
838
+ if (testFiles.length === 0) {
839
+ try {
840
+ const defaultTests = globSync('**/*.{test,spec}.{js,ts,jsx,tsx}', {
841
+ cwd: projectPath,
842
+ nodir: true,
843
+ ignore: ['node_modules/**']
844
+ });
845
+ testFiles.push(...defaultTests);
846
+
847
+ const testDirTests = globSync('**/__tests__/**/*.{js,ts,jsx,tsx}', {
848
+ cwd: projectPath,
849
+ nodir: true,
850
+ ignore: ['node_modules/**']
851
+ });
852
+ testFiles.push(...testDirTests);
853
+ } catch {}
854
+ }
855
+
856
+ return { testFiles: [...new Set(testFiles)], setupFiles: [...new Set(setupFiles)] };
857
+ }
858
+
859
+ /**
860
+ * Parse Nx workspace configuration for entry points
861
+ * Looks for project.json files in apps/ and libs/ directories
862
+ * @param {string} projectPath - Project root path
863
+ * @returns {{ entries: string[] }}
864
+ */
865
+ export function parseNxConfig(projectPath) {
866
+ const entries = [];
867
+
868
+ try {
869
+ // Find all project.json files in apps/ and libs/
870
+ const projectPatterns = [
871
+ 'apps/*/project.json',
872
+ 'apps/*/*/project.json',
873
+ 'libs/*/project.json',
874
+ 'libs/*/*/project.json',
875
+ 'packages/*/project.json'
876
+ ];
877
+
878
+ for (const pattern of projectPatterns) {
879
+ try {
880
+ const matches = globSync(pattern, { cwd: projectPath, nodir: true });
881
+ for (const match of matches) {
882
+ try {
883
+ const projectJsonPath = join(projectPath, match);
884
+ const content = JSON.parse(readFileSync(projectJsonPath, 'utf-8'));
885
+
886
+ // Only treat applications as entry points, not libraries
887
+ // Libraries are only "live" if something imports from them
888
+ const isApplication = content.projectType === 'application';
889
+ if (!isApplication) continue;
890
+
891
+ // Look for main entry in targets.build.options
892
+ if (content.targets?.build?.options?.main) {
893
+ entries.push(content.targets.build.options.main);
894
+ }
895
+
896
+ // Also check for executor-specific entries
897
+ for (const [, target] of Object.entries(content.targets || {})) {
898
+ if (target.options?.main && !entries.includes(target.options.main)) {
899
+ entries.push(target.options.main);
900
+ }
901
+ // Check for browser/server entries (Angular-style)
902
+ if (target.options?.browser) {
903
+ entries.push(target.options.browser);
904
+ }
905
+ if (target.options?.server) {
906
+ entries.push(target.options.server);
907
+ }
908
+ }
909
+ } catch {
910
+ // Ignore individual project.json parse errors
911
+ }
912
+ }
913
+ } catch {
914
+ // Ignore glob errors
915
+ }
916
+ }
917
+ } catch {
918
+ // Ignore errors
919
+ }
920
+
921
+ return { entries: [...new Set(entries)] };
922
+ }
923
+
924
+ /**
925
+ * Parse Angular workspace configuration for entry points
926
+ * @param {string} projectPath - Project root path
927
+ * @returns {{ entries: string[] }}
928
+ */
929
+ export function parseAngularConfig(projectPath) {
930
+ const entries = [];
931
+
932
+ try {
933
+ const angularJsonPath = join(projectPath, 'angular.json');
934
+ if (existsSync(angularJsonPath)) {
935
+ const content = JSON.parse(readFileSync(angularJsonPath, 'utf-8'));
936
+
937
+ for (const [, project] of Object.entries(content.projects || {})) {
938
+ // Check architect/build/options/main
939
+ if (project.architect?.build?.options?.main) {
940
+ entries.push(project.architect.build.options.main);
941
+ }
942
+ // Check for environment files in fileReplacements
943
+ if (project.architect?.build?.configurations) {
944
+ for (const [, config] of Object.entries(project.architect.build.configurations)) {
945
+ if (config.fileReplacements) {
946
+ for (const replacement of config.fileReplacements) {
947
+ if (replacement.replace) entries.push(replacement.replace);
948
+ if (replacement.with) entries.push(replacement.with);
949
+ }
950
+ }
951
+ }
952
+ }
953
+ }
954
+ }
955
+ } catch {
956
+ // Ignore errors
957
+ }
958
+
959
+ return { entries: [...new Set(entries)] };
960
+ }
961
+
962
+ /**
963
+ * Collect all entry points from bundler and CI/CD configs
964
+ * @param {string} projectPath - Project root path
965
+ * @returns {Object} - Aggregated entry point information
966
+ */
967
+ export function collectConfigEntryPoints(projectPath) {
968
+ const webpack = parseWebpackConfig(projectPath);
969
+ const vite = parseViteConfig(projectPath);
970
+ const rollup = parseRollupConfig(projectPath);
971
+ const esbuild = parseEsbuildConfig(projectPath);
972
+ const parcel = parseParcelConfig(projectPath);
973
+ const github = parseGitHubActions(projectPath);
974
+ const gitlab = parseGitLabCI(projectPath);
975
+ const jenkins = parseJenkinsfile(projectPath);
976
+ const docker = parseDockerConfig(projectPath);
977
+ const moduleFederation = parseModuleFederationConfig(projectPath);
978
+ const serverless = parseServerlessConfig(projectPath);
979
+ const nextjs = parseNextjsConfig(projectPath);
980
+ const cypress = parseCypressConfig(projectPath);
981
+ const jest = parseJestConfig(projectPath);
982
+ const nx = parseNxConfig(projectPath);
983
+ const angular = parseAngularConfig(projectPath);
984
+
985
+ // Combine all entries
986
+ const allEntries = [
987
+ ...webpack.entries,
988
+ ...vite.entries,
989
+ ...rollup.entries,
990
+ ...esbuild.entries,
991
+ ...parcel.entries,
992
+ ...github.scripts.filter(s => !s.startsWith('npm:')),
993
+ ...gitlab.scripts,
994
+ ...jenkins.scripts,
995
+ ...docker.entrypoints,
996
+ ...docker.cmdScripts,
997
+ ...moduleFederation.exposes,
998
+ ...serverless.handlers,
999
+ ...nextjs.pages,
1000
+ ...nextjs.appRoutes,
1001
+ ...nextjs.apiRoutes,
1002
+ ...cypress.specFiles,
1003
+ ...cypress.supportFiles,
1004
+ ...jest.testFiles,
1005
+ ...jest.setupFiles,
1006
+ ...nx.entries,
1007
+ ...angular.entries
1008
+ ];
1009
+
1010
+ // Normalize paths (remove leading ./)
1011
+ const normalizedEntries = allEntries.map(e =>
1012
+ e.replace(/^\.\//, '')
1013
+ );
1014
+
1015
+ return {
1016
+ bundler: {
1017
+ webpack: webpack.entries.length > 0 ? webpack : null,
1018
+ vite: vite.entries.length > 0 ? vite : null,
1019
+ rollup: rollup.entries.length > 0 ? rollup : null,
1020
+ esbuild: esbuild.entries.length > 0 ? esbuild : null,
1021
+ parcel: parcel.entries.length > 0 ? parcel : null,
1022
+ moduleFederation: moduleFederation.exposes.length > 0 ? moduleFederation : null
1023
+ },
1024
+ cicd: {
1025
+ github: github.scripts.length > 0 ? github : null,
1026
+ gitlab: gitlab.scripts.length > 0 ? gitlab : null,
1027
+ jenkins: jenkins.scripts.length > 0 ? jenkins : null,
1028
+ docker: (docker.entrypoints.length > 0 || docker.cmdScripts.length > 0) ? docker : null,
1029
+ serverless: serverless.handlers.length > 0 ? serverless : null
1030
+ },
1031
+ framework: {
1032
+ nextjs: (nextjs.pages.length > 0 || nextjs.appRoutes.length > 0) ? nextjs : null
1033
+ },
1034
+ testing: {
1035
+ cypress: (cypress.specFiles.length > 0 || cypress.supportFiles.length > 0) ? cypress : null,
1036
+ jest: (jest.testFiles.length > 0 || jest.setupFiles.length > 0) ? jest : null
1037
+ },
1038
+ entries: [...new Set(normalizedEntries)],
1039
+ npmScripts: github.scripts.filter(s => s.startsWith('npm:')).map(s => s.replace('npm:', ''))
1040
+ };
1041
+ }
1042
+
1043
+ /**
1044
+ * Check if a file is referenced in bundler/CI configs
1045
+ * @param {string} filePath - Relative file path
1046
+ * @param {Object} configData - Result from collectConfigEntryPoints
1047
+ * @returns {Object} - { isEntry: boolean, source: string|null }
1048
+ */
1049
+ export function isConfigEntry(filePath, configData) {
1050
+ const normalizedPath = filePath.replace(/^\.\//, '');
1051
+
1052
+ for (const entry of configData.entries) {
1053
+ // Direct match
1054
+ if (normalizedPath === entry || normalizedPath.endsWith(entry)) {
1055
+ return { isEntry: true, source: 'bundler/ci-config' };
1056
+ }
1057
+
1058
+ // Match without extension
1059
+ const withoutExt = entry.replace(/\.[^.]+$/, '');
1060
+ const fileWithoutExt = normalizedPath.replace(/\.[^.]+$/, '');
1061
+ if (fileWithoutExt === withoutExt || fileWithoutExt.endsWith(withoutExt)) {
1062
+ return { isEntry: true, source: 'bundler/ci-config' };
1063
+ }
1064
+ }
1065
+
1066
+ return { isEntry: false, source: null };
1067
+ }
1068
+
1069
+ export default {
1070
+ parseWebpackConfig,
1071
+ parseViteConfig,
1072
+ parseRollupConfig,
1073
+ parseEsbuildConfig,
1074
+ parseParcelConfig,
1075
+ parseGitHubActions,
1076
+ parseGitLabCI,
1077
+ parseJenkinsfile,
1078
+ parseDockerConfig,
1079
+ parseModuleFederationConfig,
1080
+ parseServerlessConfig,
1081
+ parseNextjsConfig,
1082
+ parseCypressConfig,
1083
+ parseJestConfig,
1084
+ collectConfigEntryPoints,
1085
+ isConfigEntry
1086
+ };