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,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
|
+
};
|