packlyze 4.0.0 → 4.0.2

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.
@@ -291,6 +291,325 @@ class Packlyze {
291
291
  }
292
292
  return possibleRoots;
293
293
  }
294
+ /**
295
+ * Detect missing file extensions in resolve.extensions
296
+ * Example: Error resolving './App' when App.tsx exists but .tsx not in extensions
297
+ */
298
+ detectMissingExtensions(errors) {
299
+ const missingExtensions = new Set();
300
+ for (const error of errors) {
301
+ const message = (error.message || '') + ' ' + (error.details || '');
302
+ // Pattern: Can't resolve './App' or './Component'
303
+ // Check if file exists with common extensions
304
+ const resolveError = message.match(/Can't resolve ['"]([^'"]+)['"]/);
305
+ if (resolveError) {
306
+ const importPath = resolveError[1];
307
+ // Skip if it's a package (starts with letter, no ./ or ../)
308
+ if (importPath.startsWith('./') || importPath.startsWith('../')) {
309
+ const projectRoot = this.baseDir;
310
+ const possibleExtensions = ['.tsx', '.ts', '.jsx', '.js', '.json', '.css', '.scss', '.svg', '.png'];
311
+ for (const ext of possibleExtensions) {
312
+ const possibleFile = path_1.default.resolve(projectRoot, importPath + ext);
313
+ if (fs_1.default.existsSync(possibleFile)) {
314
+ // File exists with this extension, but webpack couldn't resolve it
315
+ // This suggests the extension is missing from resolve.extensions
316
+ missingExtensions.add(ext);
317
+ }
318
+ }
319
+ }
320
+ }
321
+ }
322
+ return Array.from(missingExtensions);
323
+ }
324
+ /**
325
+ * Detect missing CSS/image loaders
326
+ */
327
+ detectMissingAssetLoaders(errors) {
328
+ let css = false;
329
+ let images = false;
330
+ for (const error of errors) {
331
+ const message = (error.message || '') + ' ' + (error.details || '');
332
+ // CSS loader errors
333
+ if (message.includes('.css') &&
334
+ (message.includes("You may need an appropriate loader") ||
335
+ message.includes("Can't resolve") && message.match(/\.css['"]/))) {
336
+ css = true;
337
+ }
338
+ // Image loader errors
339
+ if ((message.includes('.svg') || message.includes('.png') || message.includes('.jpg') || message.includes('.gif')) &&
340
+ (message.includes("You may need an appropriate loader") ||
341
+ message.includes("Can't resolve") && message.match(/\.(svg|png|jpg|jpeg|gif)['"]/))) {
342
+ images = true;
343
+ }
344
+ }
345
+ return { css, images };
346
+ }
347
+ /**
348
+ * Detect baseUrl usage without proper webpack configuration
349
+ */
350
+ detectBaseUrlIssues(errors) {
351
+ for (const error of errors) {
352
+ const message = (error.message || '') + ' ' + (error.details || '');
353
+ // Pattern: Can't resolve 'src/utils' or 'app/components' (absolute imports without ./ or ../)
354
+ // This suggests baseUrl is set in tsconfig but not configured in webpack
355
+ const absoluteImportError = message.match(/Can't resolve ['"]([^./][^'"]+)['"]/);
356
+ if (absoluteImportError) {
357
+ const importPath = absoluteImportError[1];
358
+ // Check if it's not a node_modules package (doesn't start with @ or common package names)
359
+ if (!importPath.startsWith('@') &&
360
+ !importPath.match(/^(react|vue|angular|@babel|@types)/)) {
361
+ // Check if this path exists in the project
362
+ const projectRoot = this.baseDir;
363
+ const possiblePath = path_1.default.join(projectRoot, importPath);
364
+ if (fs_1.default.existsSync(possiblePath) || fs_1.default.existsSync(possiblePath + '.ts') || fs_1.default.existsSync(possiblePath + '.tsx')) {
365
+ return true;
366
+ }
367
+ }
368
+ }
369
+ }
370
+ return false;
371
+ }
372
+ /**
373
+ * Detect case-sensitivity issues
374
+ */
375
+ detectCaseSensitivityIssues(errors) {
376
+ const issues = [];
377
+ for (const error of errors) {
378
+ const message = (error.message || '') + ' ' + (error.details || '');
379
+ // Pattern: Can't resolve './components/Button' but './Components/button.tsx' exists
380
+ const resolveError = message.match(/Can't resolve ['"]([^'"]+)['"]/);
381
+ if (resolveError) {
382
+ const importPath = resolveError[1];
383
+ if (importPath.startsWith('./') || importPath.startsWith('../')) {
384
+ const projectRoot = this.baseDir;
385
+ const fullPath = path_1.default.resolve(projectRoot, importPath);
386
+ const dir = path_1.default.dirname(fullPath);
387
+ const basename = path_1.default.basename(fullPath);
388
+ if (fs_1.default.existsSync(dir)) {
389
+ // Check for case-insensitive matches
390
+ const files = fs_1.default.readdirSync(dir);
391
+ const lowerBasename = basename.toLowerCase();
392
+ for (const file of files) {
393
+ if (file.toLowerCase() === lowerBasename && file !== basename) {
394
+ issues.push({ expected: importPath, actual: path_1.default.join(path_1.default.dirname(importPath), file) });
395
+ break;
396
+ }
397
+ }
398
+ }
399
+ }
400
+ }
401
+ }
402
+ return issues;
403
+ }
404
+ /**
405
+ * Extract alias patterns from error messages
406
+ * Example: "Can't resolve '@/hooks/useAuth'" -> { alias: '@', path: '@/hooks/useAuth' }
407
+ */
408
+ extractAliasFromErrors(errors) {
409
+ const aliases = new Map();
410
+ for (const error of errors) {
411
+ const message = (error.message || '') + ' ' + (error.details || '');
412
+ // Match patterns like '@/hooks/useAuth', '@\\hooks\\useAuth', or "'@/hooks/useAuth'"
413
+ const aliasPatterns = [
414
+ /['"`]?(@[^/\\'"`\s]+)[/\\]/g, // @/something or @\something
415
+ /Can't resolve ['"`]?(@[^/\\'"`\s]+)[/\\]/g,
416
+ /Cannot resolve ['"`]?(@[^/\\'"`\s]+)[/\\]/g,
417
+ ];
418
+ for (const pattern of aliasPatterns) {
419
+ let match;
420
+ while ((match = pattern.exec(message)) !== null) {
421
+ const alias = match[1];
422
+ if (alias && !aliases.has(alias)) {
423
+ // Extract the example path
424
+ const escapedAlias = alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
425
+ const fullPathMatch = message.match(new RegExp(`['"]?${escapedAlias}[/\\\\][^'"\\s]+`));
426
+ aliases.set(alias, fullPathMatch ? fullPathMatch[0].replace(/['"`]/g, '') : `${alias}/...`);
427
+ }
428
+ }
429
+ }
430
+ }
431
+ return Array.from(aliases.entries()).map(([alias, examplePath]) => ({ alias, examplePath }));
432
+ }
433
+ /**
434
+ * Infer alias mapping by analyzing project structure
435
+ * Example: '@' -> 'src' (most common convention)
436
+ */
437
+ inferAliasMapping(alias, examplePath) {
438
+ // Common alias mappings
439
+ const commonMappings = {
440
+ '@': ['src', 'source', 'app', 'lib'],
441
+ '~': ['src', 'source'],
442
+ '@app': ['src/app', 'app'],
443
+ '@components': ['src/components', 'components'],
444
+ '@utils': ['src/utils', 'utils'],
445
+ '@lib': ['src/lib', 'lib'],
446
+ };
447
+ // Check if we have a known mapping
448
+ if (commonMappings[alias]) {
449
+ const projectRoot = this.baseDir;
450
+ for (const possiblePath of commonMappings[alias]) {
451
+ const fullPath = path_1.default.join(projectRoot, possiblePath);
452
+ if (fs_1.default.existsSync(fullPath) && fs_1.default.statSync(fullPath).isDirectory()) {
453
+ return possiblePath;
454
+ }
455
+ }
456
+ }
457
+ // Try to infer from the example path
458
+ // If error is '@/hooks/useAuth', try to find where 'hooks' directory exists
459
+ const pathParts = examplePath.split(/[/\\]/);
460
+ if (pathParts.length >= 2) {
461
+ const firstPart = pathParts[1]; // e.g., 'hooks' from '@/hooks/useAuth'
462
+ const projectRoot = this.baseDir;
463
+ // Check common locations
464
+ const possibleLocations = [
465
+ path_1.default.join(projectRoot, 'src', firstPart),
466
+ path_1.default.join(projectRoot, firstPart),
467
+ path_1.default.join(path_1.default.dirname(projectRoot), 'src', firstPart),
468
+ ];
469
+ for (const location of possibleLocations) {
470
+ if (fs_1.default.existsSync(location) && fs_1.default.statSync(location).isDirectory()) {
471
+ // Found the directory, infer the base path
472
+ const relativePath = path_1.default.relative(projectRoot, path_1.default.dirname(location));
473
+ return relativePath || 'src'; // Default to 'src' if at root
474
+ }
475
+ }
476
+ // If we found 'hooks' in 'src/hooks', infer '@' -> 'src'
477
+ const srcHooks = path_1.default.join(projectRoot, 'src', firstPart);
478
+ if (fs_1.default.existsSync(srcHooks)) {
479
+ return 'src';
480
+ }
481
+ }
482
+ // Default inference: '@' usually maps to 'src'
483
+ if (alias === '@') {
484
+ const srcPath = path_1.default.join(this.baseDir, 'src');
485
+ if (fs_1.default.existsSync(srcPath) && fs_1.default.statSync(srcPath).isDirectory()) {
486
+ return 'src';
487
+ }
488
+ }
489
+ return null;
490
+ }
491
+ /**
492
+ * Scan source files for alias usage patterns
493
+ * Example: Finds '@/hooks/useAuth' in source files -> detects '@' alias is needed
494
+ */
495
+ scanSourceFilesForAliases() {
496
+ const aliases = new Map();
497
+ const projectRoot = this.baseDir;
498
+ // Common alias patterns to look for
499
+ const aliasPatterns = [
500
+ /['"]@\/([^'"]+)['"]/g, // '@/hooks/useAuth'
501
+ /['"]@\\([^'"]+)['"]/g, // '@\hooks\useAuth' (Windows)
502
+ /['"]~\/([^'"]+)['"]/g, // '~/components/Button'
503
+ /from\s+['"]@\/([^'"]+)['"]/g, // from '@/hooks/useAuth'
504
+ /import\s+.*from\s+['"]@\/([^'"]+)['"]/g, // import ... from '@/hooks/useAuth'
505
+ /require\(['"]@\/([^'"]+)['"]\)/g, // require('@/hooks/useAuth')
506
+ ];
507
+ // Scan common source directories
508
+ const sourceDirs = [
509
+ path_1.default.join(projectRoot, 'src'),
510
+ path_1.default.join(path_1.default.dirname(projectRoot), 'src'),
511
+ projectRoot,
512
+ ];
513
+ const scannedFiles = new Set();
514
+ const maxFilesToScan = 100; // Limit to avoid performance issues
515
+ let filesScanned = 0;
516
+ const scanDirectory = (dir, depth = 0) => {
517
+ if (depth > 5 || filesScanned >= maxFilesToScan)
518
+ return; // Limit depth and file count
519
+ if (!fs_1.default.existsSync(dir) || !fs_1.default.statSync(dir).isDirectory())
520
+ return;
521
+ try {
522
+ const entries = fs_1.default.readdirSync(dir);
523
+ for (const entry of entries) {
524
+ if (filesScanned >= maxFilesToScan)
525
+ break;
526
+ // Skip node_modules, dist, build, etc.
527
+ if (entry === 'node_modules' || entry === 'dist' || entry === 'build' ||
528
+ entry === '.git' || entry.startsWith('.')) {
529
+ continue;
530
+ }
531
+ const fullPath = path_1.default.join(dir, entry);
532
+ try {
533
+ const stats = fs_1.default.statSync(fullPath);
534
+ if (stats.isDirectory()) {
535
+ scanDirectory(fullPath, depth + 1);
536
+ }
537
+ else if (stats.isFile()) {
538
+ // Only scan JS/TS files
539
+ if (/\.(ts|tsx|js|jsx)$/.test(entry)) {
540
+ if (scannedFiles.has(fullPath))
541
+ continue;
542
+ scannedFiles.add(fullPath);
543
+ filesScanned++;
544
+ try {
545
+ const content = fs_1.default.readFileSync(fullPath, 'utf-8');
546
+ // Check for alias patterns
547
+ for (const pattern of aliasPatterns) {
548
+ let match;
549
+ while ((match = pattern.exec(content)) !== null && filesScanned < maxFilesToScan) {
550
+ const aliasPath = match[1] || match[0];
551
+ // Extract alias prefix
552
+ if (match[0].includes('@/') || match[0].includes('@\\')) {
553
+ const alias = '@';
554
+ if (!aliases.has(alias)) {
555
+ // Try to infer the mapping
556
+ const inferredPath = this.inferAliasMapping(alias, `@/${aliasPath}`);
557
+ if (inferredPath) {
558
+ const absolutePath = path_1.default.isAbsolute(inferredPath)
559
+ ? inferredPath
560
+ : path_1.default.resolve(projectRoot, inferredPath);
561
+ aliases.set(alias, absolutePath);
562
+ this.log(`Detected alias usage: ${alias} -> ${inferredPath} (from source file: ${entry})`);
563
+ }
564
+ }
565
+ }
566
+ else if (match[0].includes('~/')) {
567
+ const alias = '~';
568
+ if (!aliases.has(alias)) {
569
+ const inferredPath = this.inferAliasMapping(alias, `~/${aliasPath}`);
570
+ if (inferredPath) {
571
+ const absolutePath = path_1.default.isAbsolute(inferredPath)
572
+ ? inferredPath
573
+ : path_1.default.resolve(projectRoot, inferredPath);
574
+ aliases.set(alias, absolutePath);
575
+ this.log(`Detected alias usage: ${alias} -> ${inferredPath} (from source file: ${entry})`);
576
+ }
577
+ }
578
+ }
579
+ }
580
+ }
581
+ }
582
+ catch (error) {
583
+ // Skip files that can't be read
584
+ continue;
585
+ }
586
+ }
587
+ }
588
+ }
589
+ catch (error) {
590
+ // Skip entries that can't be accessed
591
+ continue;
592
+ }
593
+ }
594
+ }
595
+ catch (error) {
596
+ // Skip directories that can't be read
597
+ return;
598
+ }
599
+ };
600
+ // Scan all source directories
601
+ for (const sourceDir of sourceDirs) {
602
+ if (fs_1.default.existsSync(sourceDir)) {
603
+ scanDirectory(sourceDir);
604
+ }
605
+ }
606
+ // Convert Map to Record
607
+ const result = {};
608
+ for (const [key, value] of aliases.entries()) {
609
+ result[key] = value;
610
+ }
611
+ return result;
612
+ }
294
613
  /**
295
614
  * Extract TypeScript path aliases from tsconfig.json
296
615
  * Note: Does not handle "extends" - only reads the direct tsconfig.json file
@@ -485,7 +804,16 @@ class Packlyze {
485
804
  const useTypeScript = hasTypeScript;
486
805
  const isReact = framework === 'react';
487
806
  // Get TypeScript path aliases if available
488
- const pathAliases = hasTypeScript ? this.getTypeScriptPathAliases() : {};
807
+ let pathAliases = hasTypeScript ? this.getTypeScriptPathAliases() : {};
808
+ // If no aliases found in tsconfig.json, scan source files for alias usage
809
+ if (Object.keys(pathAliases).length === 0) {
810
+ this.log('No path aliases found in tsconfig.json, scanning source files for alias usage...');
811
+ const detectedAliases = this.scanSourceFilesForAliases();
812
+ if (Object.keys(detectedAliases).length > 0) {
813
+ this.log(`Detected ${Object.keys(detectedAliases).length} alias(es) from source files: ${Object.keys(detectedAliases).join(', ')}`);
814
+ pathAliases = { ...pathAliases, ...detectedAliases };
815
+ }
816
+ }
489
817
  const hasAliases = Object.keys(pathAliases).length > 0;
490
818
  // Generate alias configuration string
491
819
  let aliasConfig = '';
@@ -537,7 +865,8 @@ module.exports = {
537
865
  },
538
866
 
539
867
  resolve: {
540
- extensions: ['.js', '.jsx'${useTypeScript ? ", '.ts', '.tsx'" : ''}]${aliasConfig},
868
+ extensions: ['.js', '.jsx'${useTypeScript ? ", '.ts', '.tsx'" : ''}, '.json']${aliasConfig},
869
+ modules: ['node_modules', path.resolve(__dirname, 'src')],
541
870
  },
542
871
 
543
872
  module: {
@@ -581,6 +910,10 @@ module.exports = {
581
910
  config += ` {
582
911
  test: /\\.(png|jpg|jpeg|gif|svg)$/,
583
912
  type: 'asset/resource'
913
+ },
914
+ {
915
+ test: /\\.css$/,
916
+ use: ['style-loader', 'css-loader']
584
917
  }
585
918
  ]
586
919
  },
@@ -611,7 +944,8 @@ module.exports = {
611
944
  },
612
945
 
613
946
  resolve: {
614
- extensions: ['.js', '.jsx'${useTypeScript ? ", '.ts', '.tsx'" : ''}]${aliasConfig},
947
+ extensions: ['.js', '.jsx'${useTypeScript ? ", '.ts', '.tsx'" : ''}, '.json']${aliasConfig},
948
+ modules: ['node_modules', path.resolve(__dirname, 'src')],
615
949
  },
616
950
 
617
951
  module: {
@@ -655,6 +989,10 @@ module.exports = {
655
989
  config += ` {
656
990
  test: /\\.(png|jpg|jpeg|gif|svg)$/,
657
991
  type: 'asset/resource'
992
+ },
993
+ {
994
+ test: /\\.css$/,
995
+ use: ['style-loader', 'css-loader']
658
996
  }
659
997
  ]
660
998
  },
@@ -1066,29 +1404,162 @@ module.exports = {
1066
1404
  }
1067
1405
  }
1068
1406
  else {
1069
- // Get the search paths for better error message
1070
- const searchPaths = this.findProjectRoot();
1071
- const searchedLocations = searchPaths.map(p => path_1.default.join(p, 'tsconfig.json')).join('\n - ');
1072
- pathAliasSuggestion = `\n\n⚠️ Path alias errors detected, but no path aliases found in tsconfig.json.\n\n` +
1073
- ` Searched for tsconfig.json in:\n` +
1074
- ` - ${searchedLocations}\n\n` +
1075
- ` Make sure:\n` +
1076
- ` 1. tsconfig.json exists in one of the locations above\n` +
1077
- ` 2. tsconfig.json has compilerOptions.paths configured:\n` +
1078
- ` {\n` +
1079
- ` "compilerOptions": {\n` +
1080
- ` "baseUrl": ".",\n` +
1081
- ` "moduleResolution": "node",\n` +
1082
- ` "paths": {\n` +
1083
- ` "@/*": ["src/*"]\n` +
1084
- ` }\n` +
1085
- ` }\n` +
1086
- ` }\n` +
1087
- ` 3. If using "extends", path aliases must be in the main tsconfig.json (not just in extended config)\n` +
1088
- ` 4. If using moduleResolution: "bundler", change it to "node" or "node16" for webpack compatibility\n` +
1089
- ` 5. Run with --verbose to see detailed search logs\n\n` +
1090
- ` 💡 Alternative: Install tsconfig-paths-webpack-plugin for automatic path alias resolution:\n` +
1091
- ` npm install --save-dev tsconfig-paths-webpack-plugin\n`;
1407
+ // No aliases in tsconfig.json, but we have path alias errors
1408
+ // Try to infer aliases from error messages and auto-fix
1409
+ const inferredAliases = this.extractAliasFromErrors(pathAliasErrors);
1410
+ if (inferredAliases.length > 0) {
1411
+ this.log(`Detected ${inferredAliases.length} alias pattern(s) from error messages: ${inferredAliases.map(a => a.alias).join(', ')}`);
1412
+ // Try to infer mappings and add them to webpack config
1413
+ const aliasMappings = {};
1414
+ for (const { alias, examplePath } of inferredAliases) {
1415
+ const inferredPath = this.inferAliasMapping(alias, examplePath);
1416
+ if (inferredPath) {
1417
+ const projectRoot = this.baseDir;
1418
+ const absolutePath = path_1.default.isAbsolute(inferredPath)
1419
+ ? inferredPath
1420
+ : path_1.default.resolve(projectRoot, inferredPath);
1421
+ aliasMappings[alias] = absolutePath;
1422
+ this.log(`Inferred mapping: ${alias} -> ${inferredPath}`);
1423
+ }
1424
+ else {
1425
+ this.log(`Could not infer mapping for alias: ${alias}`);
1426
+ }
1427
+ }
1428
+ // If we found mappings, try to add them to webpack config
1429
+ if (Object.keys(aliasMappings).length > 0 && configCheck.exists) {
1430
+ try {
1431
+ // Read existing config
1432
+ const configPath = configCheck.path;
1433
+ let configContent = fs_1.default.readFileSync(configPath, 'utf-8');
1434
+ // Check if alias section exists
1435
+ const hasAliasSection = configContent.includes('resolve:') &&
1436
+ configContent.includes('alias:');
1437
+ if (!hasAliasSection) {
1438
+ // Add alias section to resolve
1439
+ const aliasEntries = Object.entries(aliasMappings)
1440
+ .map(([key, absolutePath]) => {
1441
+ const relativePath = path_1.default.relative(this.baseDir, absolutePath);
1442
+ const normalizedPath = relativePath.replace(/\\/g, '/');
1443
+ const escapedKey = key.replace(/'/g, "\\'");
1444
+ return ` '${escapedKey}': path.resolve(__dirname, '${normalizedPath}')`;
1445
+ })
1446
+ .join(',\n');
1447
+ // Try to insert alias into resolve section
1448
+ if (configContent.includes('resolve:')) {
1449
+ // Find resolve section and add alias
1450
+ const resolveMatch = configContent.match(/resolve:\s*\{[^}]*\}/s);
1451
+ if (resolveMatch) {
1452
+ const resolveSection = resolveMatch[0];
1453
+ if (!resolveSection.includes('alias:')) {
1454
+ // Add alias before closing brace
1455
+ const newResolveSection = resolveSection.replace(/\}\s*$/, `,\n alias: {\n${aliasEntries}\n }\n }`);
1456
+ configContent = configContent.replace(resolveSection, newResolveSection);
1457
+ fs_1.default.writeFileSync(configPath, configContent, 'utf-8');
1458
+ configRegenerated = true;
1459
+ pathAliasSuggestion = `\n\n✅ Auto-detected and added path aliases to ${configCheck.correctFileName}!\n` +
1460
+ ` Detected aliases from error messages:\n` +
1461
+ Object.entries(aliasMappings).map(([key, mappedPath]) => {
1462
+ const relativePath = path_1.default.relative(this.baseDir, mappedPath);
1463
+ return ` - ${key} -> ${relativePath}`;
1464
+ }).join('\n') +
1465
+ `\n\n 💡 Next steps:\n` +
1466
+ ` 1. Regenerate stats.json: npx webpack --profile --json stats.json\n` +
1467
+ ` 2. Run packlyze analyze stats.json again\n`;
1468
+ }
1469
+ }
1470
+ }
1471
+ }
1472
+ }
1473
+ catch (error) {
1474
+ this.log(`Could not auto-add aliases to config: ${error}`);
1475
+ }
1476
+ }
1477
+ // If we couldn't auto-fix, provide helpful suggestions
1478
+ if (!configRegenerated) {
1479
+ const searchPaths = this.findProjectRoot();
1480
+ const searchedLocations = searchPaths.map(p => path_1.default.join(p, 'tsconfig.json')).join('\n - ');
1481
+ const aliasExamples = inferredAliases.map(a => ` - ${a.alias} (from: ${a.examplePath})`).join('\n');
1482
+ const suggestedMappings = Object.entries(aliasMappings).length > 0
1483
+ ? `\n Inferred mappings:\n` +
1484
+ Object.entries(aliasMappings).map(([key, mappedPath]) => {
1485
+ const relativePath = path_1.default.relative(this.baseDir, mappedPath);
1486
+ return ` - ${key} -> ${relativePath}`;
1487
+ }).join('\n')
1488
+ : '';
1489
+ pathAliasSuggestion = `\n\n⚠️ Path alias errors detected, but no path aliases found in tsconfig.json.\n\n` +
1490
+ ` Detected alias patterns from errors:\n${aliasExamples}\n` +
1491
+ (suggestedMappings || '') +
1492
+ `\n Searched for tsconfig.json in:\n` +
1493
+ ` - ${searchedLocations}\n\n` +
1494
+ (Object.keys(aliasMappings).length > 0 ?
1495
+ ` 💡 Quick fix - Add to your ${configCheck.correctFileName}:\n` +
1496
+ ` resolve: {\n` +
1497
+ ` alias: {\n` +
1498
+ Object.entries(aliasMappings).map(([key, mappedPath]) => {
1499
+ const relativePath = path_1.default.relative(this.baseDir, mappedPath);
1500
+ const normalizedPath = relativePath.replace(/\\/g, '/');
1501
+ return ` '${key}': path.resolve(__dirname, '${normalizedPath}')`;
1502
+ }).join(',\n') +
1503
+ `\n }\n` +
1504
+ ` }\n\n` +
1505
+ ` Or add to tsconfig.json:\n` +
1506
+ ` {\n` +
1507
+ ` "compilerOptions": {\n` +
1508
+ ` "baseUrl": ".",\n` +
1509
+ ` "moduleResolution": "node",\n` +
1510
+ ` "paths": {\n` +
1511
+ Object.entries(aliasMappings).map(([key, mappedPath]) => {
1512
+ const relativePath = path_1.default.relative(this.baseDir, mappedPath);
1513
+ const normalizedPath = relativePath.replace(/\\/g, '/');
1514
+ return ` "${key}/*": ["${normalizedPath}/*"]`;
1515
+ }).join(',\n') +
1516
+ `\n }\n` +
1517
+ ` }\n` +
1518
+ ` }\n` :
1519
+ ` Make sure:\n` +
1520
+ ` 1. tsconfig.json exists in one of the locations above\n` +
1521
+ ` 2. tsconfig.json has compilerOptions.paths configured:\n` +
1522
+ ` {\n` +
1523
+ ` "compilerOptions": {\n` +
1524
+ ` "baseUrl": ".",\n` +
1525
+ ` "moduleResolution": "node",\n` +
1526
+ ` "paths": {\n` +
1527
+ ` "@/*": ["src/*"]\n` +
1528
+ ` }\n` +
1529
+ ` }\n` +
1530
+ ` }\n` +
1531
+ ` 3. If using "extends", path aliases must be in the main tsconfig.json (not just in extended config)\n` +
1532
+ ` 4. If using moduleResolution: "bundler", change it to "node" or "node16" for webpack compatibility\n` +
1533
+ ` 5. Run with --verbose to see detailed search logs\n\n` +
1534
+ ` 💡 Alternative: Install tsconfig-paths-webpack-plugin for automatic path alias resolution:\n` +
1535
+ ` npm install --save-dev tsconfig-paths-webpack-plugin\n`);
1536
+ }
1537
+ }
1538
+ else {
1539
+ // Could not extract aliases from errors
1540
+ const searchPaths = this.findProjectRoot();
1541
+ const searchedLocations = searchPaths.map(p => path_1.default.join(p, 'tsconfig.json')).join('\n - ');
1542
+ pathAliasSuggestion = `\n\n⚠️ Path alias errors detected, but no path aliases found in tsconfig.json.\n\n` +
1543
+ ` Searched for tsconfig.json in:\n` +
1544
+ ` - ${searchedLocations}\n\n` +
1545
+ ` Make sure:\n` +
1546
+ ` 1. tsconfig.json exists in one of the locations above\n` +
1547
+ ` 2. tsconfig.json has compilerOptions.paths configured:\n` +
1548
+ ` {\n` +
1549
+ ` "compilerOptions": {\n` +
1550
+ ` "baseUrl": ".",\n` +
1551
+ ` "moduleResolution": "node",\n` +
1552
+ ` "paths": {\n` +
1553
+ ` "@/*": ["src/*"]\n` +
1554
+ ` }\n` +
1555
+ ` }\n` +
1556
+ ` }\n` +
1557
+ ` 3. If using "extends", path aliases must be in the main tsconfig.json (not just in extended config)\n` +
1558
+ ` 4. If using moduleResolution: "bundler", change it to "node" or "node16" for webpack compatibility\n` +
1559
+ ` 5. Run with --verbose to see detailed search logs\n\n` +
1560
+ ` 💡 Alternative: Install tsconfig-paths-webpack-plugin for automatic path alias resolution:\n` +
1561
+ ` npm install --save-dev tsconfig-paths-webpack-plugin\n`;
1562
+ }
1092
1563
  }
1093
1564
  }
1094
1565
  // Build the error message
@@ -1110,8 +1581,138 @@ module.exports = {
1110
1581
  if (!configRegenerated && pathAliasSuggestion) {
1111
1582
  errorMessage += pathAliasSuggestion;
1112
1583
  }
1584
+ // Detect and handle other common issues
1585
+ let otherIssues = '';
1586
+ // 1. Missing extensions in resolve.extensions
1587
+ const missingExtensions = this.detectMissingExtensions(errors);
1588
+ if (missingExtensions.length > 0) {
1589
+ const configCheck = this.checkWebpackConfig();
1590
+ if (configCheck.exists && configCheck.path) {
1591
+ try {
1592
+ let configContent = fs_1.default.readFileSync(configCheck.path, 'utf-8');
1593
+ // Check if extensions are already in config
1594
+ const hasExtensions = configContent.includes('extensions:');
1595
+ if (hasExtensions) {
1596
+ // Try to add missing extensions
1597
+ const extensionsMatch = configContent.match(/extensions:\s*\[([^\]]+)\]/);
1598
+ if (extensionsMatch) {
1599
+ const existingExtensions = extensionsMatch[1];
1600
+ const newExtensions = missingExtensions
1601
+ .filter(ext => !existingExtensions.includes(ext))
1602
+ .map(ext => `'${ext}'`)
1603
+ .join(', ');
1604
+ if (newExtensions) {
1605
+ const newExtensionsList = existingExtensions.trim()
1606
+ ? `${existingExtensions}, ${newExtensions}`
1607
+ : newExtensions;
1608
+ configContent = configContent.replace(/extensions:\s*\[([^\]]+)\]/, `extensions: [${newExtensionsList}]`);
1609
+ fs_1.default.writeFileSync(configCheck.path, configContent, 'utf-8');
1610
+ otherIssues += `\n\n✅ Auto-added missing extensions to resolve.extensions: ${missingExtensions.join(', ')}\n`;
1611
+ }
1612
+ }
1613
+ }
1614
+ else {
1615
+ // Add extensions section
1616
+ if (configContent.includes('resolve:')) {
1617
+ const resolveMatch = configContent.match(/resolve:\s*\{[^}]*\}/s);
1618
+ if (resolveMatch) {
1619
+ const extensionsList = missingExtensions.map(ext => `'${ext}'`).join(', ');
1620
+ const newResolveSection = resolveMatch[0].replace(/\}\s*$/, `,\n extensions: ['.js', '.jsx', '.ts', '.tsx', ${extensionsList}]\n }`);
1621
+ configContent = configContent.replace(resolveMatch[0], newResolveSection);
1622
+ fs_1.default.writeFileSync(configCheck.path, configContent, 'utf-8');
1623
+ otherIssues += `\n\n✅ Auto-added resolve.extensions: ${missingExtensions.join(', ')}\n`;
1624
+ }
1625
+ }
1626
+ }
1627
+ }
1628
+ catch (error) {
1629
+ otherIssues += `\n\n⚠️ Missing file extensions in resolve.extensions: ${missingExtensions.join(', ')}\n` +
1630
+ ` Add to your webpack config:\n` +
1631
+ ` resolve: {\n` +
1632
+ ` extensions: ['.js', '.jsx', '.ts', '.tsx', ${missingExtensions.map(e => `'${e}'`).join(', ')}]\n` +
1633
+ ` }\n`;
1634
+ }
1635
+ }
1636
+ else {
1637
+ otherIssues += `\n\n⚠️ Missing file extensions in resolve.extensions: ${missingExtensions.join(', ')}\n` +
1638
+ ` Add these extensions to your webpack config's resolve.extensions array.\n`;
1639
+ }
1640
+ }
1641
+ // 2. Missing CSS/image loaders
1642
+ const missingLoaders = this.detectMissingAssetLoaders(errors);
1643
+ if (missingLoaders.css || missingLoaders.images) {
1644
+ const missingPackages = [];
1645
+ if (missingLoaders.css) {
1646
+ missingPackages.push('css-loader', 'style-loader');
1647
+ }
1648
+ if (missingLoaders.images) {
1649
+ // Webpack 5 uses asset/resource, no loader needed, but we should suggest it
1650
+ }
1651
+ if (missingPackages.length > 0) {
1652
+ const dependencyCheck = this.checkPackagesInstalled(missingPackages);
1653
+ if (dependencyCheck && dependencyCheck.missing.length > 0) {
1654
+ const installCommand = this.generateInstallCommand(dependencyCheck.missing);
1655
+ otherIssues += `\n\n⚠️ Missing CSS/Asset loaders detected!\n` +
1656
+ ` The following packages are required but not installed:\n` +
1657
+ ` ${dependencyCheck.missing.map(p => ` - ${p}`).join('\n')}\n\n` +
1658
+ ` 💡 Install them with:\n` +
1659
+ ` ${installCommand}\n\n` +
1660
+ ` Then add to your webpack config:\n` +
1661
+ (missingLoaders.css ?
1662
+ ` module: {\n` +
1663
+ ` rules: [\n` +
1664
+ ` { test: /\\.css$/, use: ['style-loader', 'css-loader'] }\n` +
1665
+ ` ]\n` +
1666
+ ` }\n` : '');
1667
+ }
1668
+ }
1669
+ }
1670
+ // 3. baseUrl issues
1671
+ const hasBaseUrlIssue = this.detectBaseUrlIssues(errors);
1672
+ if (hasBaseUrlIssue) {
1673
+ const tsconfigPath = this.findProjectRoot().map(p => path_1.default.join(p, 'tsconfig.json')).find(p => fs_1.default.existsSync(p));
1674
+ if (tsconfigPath) {
1675
+ try {
1676
+ const tsconfigContent = fs_1.default.readFileSync(tsconfigPath, 'utf-8');
1677
+ const cleanedContent = tsconfigContent.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '');
1678
+ const tsconfig = JSON.parse(cleanedContent);
1679
+ if (tsconfig.compilerOptions?.baseUrl && !tsconfig.compilerOptions?.paths) {
1680
+ otherIssues += `\n\n⚠️ baseUrl is set in tsconfig.json but paths are not configured.\n` +
1681
+ ` TypeScript can resolve imports like 'src/utils', but webpack cannot.\n\n` +
1682
+ ` 💡 Solution 1: Add resolve.modules to webpack config:\n` +
1683
+ ` resolve: {\n` +
1684
+ ` modules: ['node_modules', '${tsconfig.compilerOptions.baseUrl}']\n` +
1685
+ ` }\n\n` +
1686
+ ` 💡 Solution 2: Add paths to tsconfig.json:\n` +
1687
+ ` "compilerOptions": {\n` +
1688
+ ` "baseUrl": ".",\n` +
1689
+ ` "paths": {\n` +
1690
+ ` "src/*": ["src/*"],\n` +
1691
+ ` "app/*": ["app/*"]\n` +
1692
+ ` }\n` +
1693
+ ` }\n`;
1694
+ }
1695
+ }
1696
+ catch (error) {
1697
+ // Ignore parse errors
1698
+ }
1699
+ }
1700
+ }
1701
+ // 4. Case-sensitivity issues
1702
+ const caseIssues = this.detectCaseSensitivityIssues(errors);
1703
+ if (caseIssues.length > 0) {
1704
+ otherIssues += `\n\n⚠️ Case-sensitivity issues detected!\n` +
1705
+ ` Your imports use different casing than the actual files:\n` +
1706
+ caseIssues.map(issue => ` - Imported: ${issue.expected}\n Actual: ${issue.actual}`).join('\n') +
1707
+ `\n\n 💡 Fix: Update your imports to match the exact file casing.\n` +
1708
+ ` This is especially important for Linux/CI environments where file systems are case-sensitive.\n`;
1709
+ }
1710
+ // Add other issues to error message
1711
+ if (otherIssues) {
1712
+ errorMessage += otherIssues;
1713
+ }
1113
1714
  // Add default suggestion if no other suggestions
1114
- if (!suggestion && !pathAliasSuggestion && !dependencySuggestion) {
1715
+ if (!suggestion && !pathAliasSuggestion && !dependencySuggestion && !otherIssues) {
1115
1716
  errorMessage += `\n💡 Fix the webpack build errors first, then regenerate stats.json:\n` +
1116
1717
  ` npx webpack --profile --json stats.json`;
1117
1718
  }