packlyze 3.0.9 → 4.0.1

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.
@@ -53,14 +53,14 @@ class Packlyze {
53
53
  return null;
54
54
  }
55
55
  // Priority order for entry point detection
56
+ // main.* files are typically the actual entry points, so check them first
56
57
  const entryPointPatterns = [
57
- // React (case-sensitive, most common)
58
+ // Main entry points (highest priority - these are typically the actual entry points)
59
+ 'main.tsx', 'main.ts', 'main.jsx', 'main.js',
60
+ // React App files (common but not always the entry point)
58
61
  'App.tsx', 'App.jsx', 'app.tsx', 'app.jsx',
59
- // React index files
60
- 'index.tsx', 'index.jsx',
61
- // Generic entry points
62
- 'main.ts', 'main.js', 'main.tsx', 'main.jsx',
63
- 'index.ts', 'index.js',
62
+ // Index files (fallback entry points)
63
+ 'index.tsx', 'index.ts', 'index.jsx', 'index.js',
64
64
  // Vue/Angular
65
65
  'app.ts', 'app.js',
66
66
  ];
@@ -291,6 +291,203 @@ 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
+ }
294
491
  /**
295
492
  * Extract TypeScript path aliases from tsconfig.json
296
493
  * Note: Does not handle "extends" - only reads the direct tsconfig.json file
@@ -321,6 +518,12 @@ class Packlyze {
321
518
  this.log(`Warning: tsconfig.json found but compilerOptions is missing`);
322
519
  continue;
323
520
  }
521
+ // Check for moduleResolution: "bundler" which doesn't work well with webpack
522
+ const moduleResolution = tsconfig.compilerOptions.moduleResolution;
523
+ if (moduleResolution === 'bundler') {
524
+ this.log(`Warning: moduleResolution: "bundler" is set in tsconfig.json. This works with Vite/esbuild but not webpack.`);
525
+ this.log(`Consider using "node" or "node16" for webpack compatibility.`);
526
+ }
324
527
  const paths = tsconfig.compilerOptions.paths;
325
528
  if (paths && typeof paths === 'object') {
326
529
  this.log(`Found compilerOptions.paths in tsconfig.json`);
@@ -513,6 +716,9 @@ class Packlyze {
513
716
  })
514
717
  .join(',\n');
515
718
  aliasConfig = `,\n alias: {\n${aliasEntries}\n }`;
719
+ // Add note about tsconfig-paths-webpack-plugin as alternative
720
+ this.log(`Note: Using webpack resolve.alias for path aliases.`);
721
+ this.log(`Alternative: You can use tsconfig-paths-webpack-plugin for automatic sync with tsconfig.json`);
516
722
  }
517
723
  let config = '';
518
724
  if (isESModule) {
@@ -528,21 +734,31 @@ module.exports = {
528
734
  },
529
735
 
530
736
  resolve: {
531
- extensions: ['.js', '.jsx'${useTypeScript ? ", '.ts', '.tsx'" : ''}]${aliasConfig},
737
+ extensions: ['.js', '.jsx'${useTypeScript ? ", '.ts', '.tsx'" : ''}, '.json']${aliasConfig},
738
+ modules: ['node_modules', path.resolve(__dirname, 'src')],
532
739
  },
533
740
 
534
741
  module: {
535
742
  rules: [
536
743
  `;
537
744
  if (useTypeScript) {
745
+ // Configure ts-loader to work better with path aliases
746
+ // Note: transpileOnly: true speeds up builds but webpack still needs resolve.alias for path resolution
747
+ const tsLoaderOptions = hasAliases
748
+ ? `{
749
+ transpileOnly: true,
750
+ // Path aliases are handled by webpack resolve.alias above
751
+ // TypeScript type checking happens separately (e.g., via tsc --noEmit)
752
+ }`
753
+ : `{
754
+ transpileOnly: true
755
+ }`;
538
756
  config += ` {
539
757
  test: /\\.(ts|tsx)$/,
540
758
  exclude: /node_modules/,
541
759
  use: {
542
760
  loader: 'ts-loader',
543
- options: {
544
- transpileOnly: true
545
- }
761
+ options: ${tsLoaderOptions}
546
762
  }
547
763
  },
548
764
  `;
@@ -563,6 +779,10 @@ module.exports = {
563
779
  config += ` {
564
780
  test: /\\.(png|jpg|jpeg|gif|svg)$/,
565
781
  type: 'asset/resource'
782
+ },
783
+ {
784
+ test: /\\.css$/,
785
+ use: ['style-loader', 'css-loader']
566
786
  }
567
787
  ]
568
788
  },
@@ -593,21 +813,31 @@ module.exports = {
593
813
  },
594
814
 
595
815
  resolve: {
596
- extensions: ['.js', '.jsx'${useTypeScript ? ", '.ts', '.tsx'" : ''}]${aliasConfig},
816
+ extensions: ['.js', '.jsx'${useTypeScript ? ", '.ts', '.tsx'" : ''}, '.json']${aliasConfig},
817
+ modules: ['node_modules', path.resolve(__dirname, 'src')],
597
818
  },
598
819
 
599
820
  module: {
600
821
  rules: [
601
822
  `;
602
823
  if (useTypeScript) {
824
+ // Configure ts-loader to work better with path aliases
825
+ // Note: transpileOnly: true speeds up builds but webpack still needs resolve.alias for path resolution
826
+ const tsLoaderOptions = hasAliases
827
+ ? `{
828
+ transpileOnly: true,
829
+ // Path aliases are handled by webpack resolve.alias above
830
+ // TypeScript type checking happens separately (e.g., via tsc --noEmit)
831
+ }`
832
+ : `{
833
+ transpileOnly: true
834
+ }`;
603
835
  config += ` {
604
836
  test: /\\.(ts|tsx)$/,
605
837
  exclude: /node_modules/,
606
838
  use: {
607
839
  loader: 'ts-loader',
608
- options: {
609
- transpileOnly: true
610
- }
840
+ options: ${tsLoaderOptions}
611
841
  }
612
842
  },
613
843
  `;
@@ -628,6 +858,10 @@ module.exports = {
628
858
  config += ` {
629
859
  test: /\\.(png|jpg|jpeg|gif|svg)$/,
630
860
  type: 'asset/resource'
861
+ },
862
+ {
863
+ test: /\\.css$/,
864
+ use: ['style-loader', 'css-loader']
631
865
  }
632
866
  ]
633
867
  },
@@ -1039,25 +1273,162 @@ module.exports = {
1039
1273
  }
1040
1274
  }
1041
1275
  else {
1042
- // Get the search paths for better error message
1043
- const searchPaths = this.findProjectRoot();
1044
- const searchedLocations = searchPaths.map(p => path_1.default.join(p, 'tsconfig.json')).join('\n - ');
1045
- pathAliasSuggestion = `\n\n⚠️ Path alias errors detected, but no path aliases found in tsconfig.json.\n\n` +
1046
- ` Searched for tsconfig.json in:\n` +
1047
- ` - ${searchedLocations}\n\n` +
1048
- ` Make sure:\n` +
1049
- ` 1. tsconfig.json exists in one of the locations above\n` +
1050
- ` 2. tsconfig.json has compilerOptions.paths configured:\n` +
1051
- ` {\n` +
1052
- ` "compilerOptions": {\n` +
1053
- ` "baseUrl": ".",\n` +
1054
- ` "paths": {\n` +
1055
- ` "@/*": ["src/*"]\n` +
1056
- ` }\n` +
1057
- ` }\n` +
1058
- ` }\n` +
1059
- ` 3. If using "extends", path aliases must be in the main tsconfig.json (not just in extended config)\n` +
1060
- ` 4. Run with --verbose to see detailed search logs\n`;
1276
+ // No aliases in tsconfig.json, but we have path alias errors
1277
+ // Try to infer aliases from error messages and auto-fix
1278
+ const inferredAliases = this.extractAliasFromErrors(pathAliasErrors);
1279
+ if (inferredAliases.length > 0) {
1280
+ this.log(`Detected ${inferredAliases.length} alias pattern(s) from error messages: ${inferredAliases.map(a => a.alias).join(', ')}`);
1281
+ // Try to infer mappings and add them to webpack config
1282
+ const aliasMappings = {};
1283
+ for (const { alias, examplePath } of inferredAliases) {
1284
+ const inferredPath = this.inferAliasMapping(alias, examplePath);
1285
+ if (inferredPath) {
1286
+ const projectRoot = this.baseDir;
1287
+ const absolutePath = path_1.default.isAbsolute(inferredPath)
1288
+ ? inferredPath
1289
+ : path_1.default.resolve(projectRoot, inferredPath);
1290
+ aliasMappings[alias] = absolutePath;
1291
+ this.log(`Inferred mapping: ${alias} -> ${inferredPath}`);
1292
+ }
1293
+ else {
1294
+ this.log(`Could not infer mapping for alias: ${alias}`);
1295
+ }
1296
+ }
1297
+ // If we found mappings, try to add them to webpack config
1298
+ if (Object.keys(aliasMappings).length > 0 && configCheck.exists) {
1299
+ try {
1300
+ // Read existing config
1301
+ const configPath = configCheck.path;
1302
+ let configContent = fs_1.default.readFileSync(configPath, 'utf-8');
1303
+ // Check if alias section exists
1304
+ const hasAliasSection = configContent.includes('resolve:') &&
1305
+ configContent.includes('alias:');
1306
+ if (!hasAliasSection) {
1307
+ // Add alias section to resolve
1308
+ const aliasEntries = Object.entries(aliasMappings)
1309
+ .map(([key, absolutePath]) => {
1310
+ const relativePath = path_1.default.relative(this.baseDir, absolutePath);
1311
+ const normalizedPath = relativePath.replace(/\\/g, '/');
1312
+ const escapedKey = key.replace(/'/g, "\\'");
1313
+ return ` '${escapedKey}': path.resolve(__dirname, '${normalizedPath}')`;
1314
+ })
1315
+ .join(',\n');
1316
+ // Try to insert alias into resolve section
1317
+ if (configContent.includes('resolve:')) {
1318
+ // Find resolve section and add alias
1319
+ const resolveMatch = configContent.match(/resolve:\s*\{[^}]*\}/s);
1320
+ if (resolveMatch) {
1321
+ const resolveSection = resolveMatch[0];
1322
+ if (!resolveSection.includes('alias:')) {
1323
+ // Add alias before closing brace
1324
+ const newResolveSection = resolveSection.replace(/\}\s*$/, `,\n alias: {\n${aliasEntries}\n }\n }`);
1325
+ configContent = configContent.replace(resolveSection, newResolveSection);
1326
+ fs_1.default.writeFileSync(configPath, configContent, 'utf-8');
1327
+ configRegenerated = true;
1328
+ pathAliasSuggestion = `\n\n✅ Auto-detected and added path aliases to ${configCheck.correctFileName}!\n` +
1329
+ ` Detected aliases from error messages:\n` +
1330
+ Object.entries(aliasMappings).map(([key, mappedPath]) => {
1331
+ const relativePath = path_1.default.relative(this.baseDir, mappedPath);
1332
+ return ` - ${key} -> ${relativePath}`;
1333
+ }).join('\n') +
1334
+ `\n\n 💡 Next steps:\n` +
1335
+ ` 1. Regenerate stats.json: npx webpack --profile --json stats.json\n` +
1336
+ ` 2. Run packlyze analyze stats.json again\n`;
1337
+ }
1338
+ }
1339
+ }
1340
+ }
1341
+ }
1342
+ catch (error) {
1343
+ this.log(`Could not auto-add aliases to config: ${error}`);
1344
+ }
1345
+ }
1346
+ // If we couldn't auto-fix, provide helpful suggestions
1347
+ if (!configRegenerated) {
1348
+ const searchPaths = this.findProjectRoot();
1349
+ const searchedLocations = searchPaths.map(p => path_1.default.join(p, 'tsconfig.json')).join('\n - ');
1350
+ const aliasExamples = inferredAliases.map(a => ` - ${a.alias} (from: ${a.examplePath})`).join('\n');
1351
+ const suggestedMappings = Object.entries(aliasMappings).length > 0
1352
+ ? `\n Inferred mappings:\n` +
1353
+ Object.entries(aliasMappings).map(([key, mappedPath]) => {
1354
+ const relativePath = path_1.default.relative(this.baseDir, mappedPath);
1355
+ return ` - ${key} -> ${relativePath}`;
1356
+ }).join('\n')
1357
+ : '';
1358
+ pathAliasSuggestion = `\n\n⚠️ Path alias errors detected, but no path aliases found in tsconfig.json.\n\n` +
1359
+ ` Detected alias patterns from errors:\n${aliasExamples}\n` +
1360
+ (suggestedMappings || '') +
1361
+ `\n Searched for tsconfig.json in:\n` +
1362
+ ` - ${searchedLocations}\n\n` +
1363
+ (Object.keys(aliasMappings).length > 0 ?
1364
+ ` 💡 Quick fix - Add to your ${configCheck.correctFileName}:\n` +
1365
+ ` resolve: {\n` +
1366
+ ` alias: {\n` +
1367
+ Object.entries(aliasMappings).map(([key, mappedPath]) => {
1368
+ const relativePath = path_1.default.relative(this.baseDir, mappedPath);
1369
+ const normalizedPath = relativePath.replace(/\\/g, '/');
1370
+ return ` '${key}': path.resolve(__dirname, '${normalizedPath}')`;
1371
+ }).join(',\n') +
1372
+ `\n }\n` +
1373
+ ` }\n\n` +
1374
+ ` Or add to tsconfig.json:\n` +
1375
+ ` {\n` +
1376
+ ` "compilerOptions": {\n` +
1377
+ ` "baseUrl": ".",\n` +
1378
+ ` "moduleResolution": "node",\n` +
1379
+ ` "paths": {\n` +
1380
+ Object.entries(aliasMappings).map(([key, mappedPath]) => {
1381
+ const relativePath = path_1.default.relative(this.baseDir, mappedPath);
1382
+ const normalizedPath = relativePath.replace(/\\/g, '/');
1383
+ return ` "${key}/*": ["${normalizedPath}/*"]`;
1384
+ }).join(',\n') +
1385
+ `\n }\n` +
1386
+ ` }\n` +
1387
+ ` }\n` :
1388
+ ` Make sure:\n` +
1389
+ ` 1. tsconfig.json exists in one of the locations above\n` +
1390
+ ` 2. tsconfig.json has compilerOptions.paths configured:\n` +
1391
+ ` {\n` +
1392
+ ` "compilerOptions": {\n` +
1393
+ ` "baseUrl": ".",\n` +
1394
+ ` "moduleResolution": "node",\n` +
1395
+ ` "paths": {\n` +
1396
+ ` "@/*": ["src/*"]\n` +
1397
+ ` }\n` +
1398
+ ` }\n` +
1399
+ ` }\n` +
1400
+ ` 3. If using "extends", path aliases must be in the main tsconfig.json (not just in extended config)\n` +
1401
+ ` 4. If using moduleResolution: "bundler", change it to "node" or "node16" for webpack compatibility\n` +
1402
+ ` 5. Run with --verbose to see detailed search logs\n\n` +
1403
+ ` 💡 Alternative: Install tsconfig-paths-webpack-plugin for automatic path alias resolution:\n` +
1404
+ ` npm install --save-dev tsconfig-paths-webpack-plugin\n`);
1405
+ }
1406
+ }
1407
+ else {
1408
+ // Could not extract aliases from errors
1409
+ const searchPaths = this.findProjectRoot();
1410
+ const searchedLocations = searchPaths.map(p => path_1.default.join(p, 'tsconfig.json')).join('\n - ');
1411
+ pathAliasSuggestion = `\n\n⚠️ Path alias errors detected, but no path aliases found in tsconfig.json.\n\n` +
1412
+ ` Searched for tsconfig.json in:\n` +
1413
+ ` - ${searchedLocations}\n\n` +
1414
+ ` Make sure:\n` +
1415
+ ` 1. tsconfig.json exists in one of the locations above\n` +
1416
+ ` 2. tsconfig.json has compilerOptions.paths configured:\n` +
1417
+ ` {\n` +
1418
+ ` "compilerOptions": {\n` +
1419
+ ` "baseUrl": ".",\n` +
1420
+ ` "moduleResolution": "node",\n` +
1421
+ ` "paths": {\n` +
1422
+ ` "@/*": ["src/*"]\n` +
1423
+ ` }\n` +
1424
+ ` }\n` +
1425
+ ` }\n` +
1426
+ ` 3. If using "extends", path aliases must be in the main tsconfig.json (not just in extended config)\n` +
1427
+ ` 4. If using moduleResolution: "bundler", change it to "node" or "node16" for webpack compatibility\n` +
1428
+ ` 5. Run with --verbose to see detailed search logs\n\n` +
1429
+ ` 💡 Alternative: Install tsconfig-paths-webpack-plugin for automatic path alias resolution:\n` +
1430
+ ` npm install --save-dev tsconfig-paths-webpack-plugin\n`;
1431
+ }
1061
1432
  }
1062
1433
  }
1063
1434
  // Build the error message
@@ -1079,8 +1450,138 @@ module.exports = {
1079
1450
  if (!configRegenerated && pathAliasSuggestion) {
1080
1451
  errorMessage += pathAliasSuggestion;
1081
1452
  }
1453
+ // Detect and handle other common issues
1454
+ let otherIssues = '';
1455
+ // 1. Missing extensions in resolve.extensions
1456
+ const missingExtensions = this.detectMissingExtensions(errors);
1457
+ if (missingExtensions.length > 0) {
1458
+ const configCheck = this.checkWebpackConfig();
1459
+ if (configCheck.exists && configCheck.path) {
1460
+ try {
1461
+ let configContent = fs_1.default.readFileSync(configCheck.path, 'utf-8');
1462
+ // Check if extensions are already in config
1463
+ const hasExtensions = configContent.includes('extensions:');
1464
+ if (hasExtensions) {
1465
+ // Try to add missing extensions
1466
+ const extensionsMatch = configContent.match(/extensions:\s*\[([^\]]+)\]/);
1467
+ if (extensionsMatch) {
1468
+ const existingExtensions = extensionsMatch[1];
1469
+ const newExtensions = missingExtensions
1470
+ .filter(ext => !existingExtensions.includes(ext))
1471
+ .map(ext => `'${ext}'`)
1472
+ .join(', ');
1473
+ if (newExtensions) {
1474
+ const newExtensionsList = existingExtensions.trim()
1475
+ ? `${existingExtensions}, ${newExtensions}`
1476
+ : newExtensions;
1477
+ configContent = configContent.replace(/extensions:\s*\[([^\]]+)\]/, `extensions: [${newExtensionsList}]`);
1478
+ fs_1.default.writeFileSync(configCheck.path, configContent, 'utf-8');
1479
+ otherIssues += `\n\n✅ Auto-added missing extensions to resolve.extensions: ${missingExtensions.join(', ')}\n`;
1480
+ }
1481
+ }
1482
+ }
1483
+ else {
1484
+ // Add extensions section
1485
+ if (configContent.includes('resolve:')) {
1486
+ const resolveMatch = configContent.match(/resolve:\s*\{[^}]*\}/s);
1487
+ if (resolveMatch) {
1488
+ const extensionsList = missingExtensions.map(ext => `'${ext}'`).join(', ');
1489
+ const newResolveSection = resolveMatch[0].replace(/\}\s*$/, `,\n extensions: ['.js', '.jsx', '.ts', '.tsx', ${extensionsList}]\n }`);
1490
+ configContent = configContent.replace(resolveMatch[0], newResolveSection);
1491
+ fs_1.default.writeFileSync(configCheck.path, configContent, 'utf-8');
1492
+ otherIssues += `\n\n✅ Auto-added resolve.extensions: ${missingExtensions.join(', ')}\n`;
1493
+ }
1494
+ }
1495
+ }
1496
+ }
1497
+ catch (error) {
1498
+ otherIssues += `\n\n⚠️ Missing file extensions in resolve.extensions: ${missingExtensions.join(', ')}\n` +
1499
+ ` Add to your webpack config:\n` +
1500
+ ` resolve: {\n` +
1501
+ ` extensions: ['.js', '.jsx', '.ts', '.tsx', ${missingExtensions.map(e => `'${e}'`).join(', ')}]\n` +
1502
+ ` }\n`;
1503
+ }
1504
+ }
1505
+ else {
1506
+ otherIssues += `\n\n⚠️ Missing file extensions in resolve.extensions: ${missingExtensions.join(', ')}\n` +
1507
+ ` Add these extensions to your webpack config's resolve.extensions array.\n`;
1508
+ }
1509
+ }
1510
+ // 2. Missing CSS/image loaders
1511
+ const missingLoaders = this.detectMissingAssetLoaders(errors);
1512
+ if (missingLoaders.css || missingLoaders.images) {
1513
+ const missingPackages = [];
1514
+ if (missingLoaders.css) {
1515
+ missingPackages.push('css-loader', 'style-loader');
1516
+ }
1517
+ if (missingLoaders.images) {
1518
+ // Webpack 5 uses asset/resource, no loader needed, but we should suggest it
1519
+ }
1520
+ if (missingPackages.length > 0) {
1521
+ const dependencyCheck = this.checkPackagesInstalled(missingPackages);
1522
+ if (dependencyCheck && dependencyCheck.missing.length > 0) {
1523
+ const installCommand = this.generateInstallCommand(dependencyCheck.missing);
1524
+ otherIssues += `\n\n⚠️ Missing CSS/Asset loaders detected!\n` +
1525
+ ` The following packages are required but not installed:\n` +
1526
+ ` ${dependencyCheck.missing.map(p => ` - ${p}`).join('\n')}\n\n` +
1527
+ ` 💡 Install them with:\n` +
1528
+ ` ${installCommand}\n\n` +
1529
+ ` Then add to your webpack config:\n` +
1530
+ (missingLoaders.css ?
1531
+ ` module: {\n` +
1532
+ ` rules: [\n` +
1533
+ ` { test: /\\.css$/, use: ['style-loader', 'css-loader'] }\n` +
1534
+ ` ]\n` +
1535
+ ` }\n` : '');
1536
+ }
1537
+ }
1538
+ }
1539
+ // 3. baseUrl issues
1540
+ const hasBaseUrlIssue = this.detectBaseUrlIssues(errors);
1541
+ if (hasBaseUrlIssue) {
1542
+ const tsconfigPath = this.findProjectRoot().map(p => path_1.default.join(p, 'tsconfig.json')).find(p => fs_1.default.existsSync(p));
1543
+ if (tsconfigPath) {
1544
+ try {
1545
+ const tsconfigContent = fs_1.default.readFileSync(tsconfigPath, 'utf-8');
1546
+ const cleanedContent = tsconfigContent.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '');
1547
+ const tsconfig = JSON.parse(cleanedContent);
1548
+ if (tsconfig.compilerOptions?.baseUrl && !tsconfig.compilerOptions?.paths) {
1549
+ otherIssues += `\n\n⚠️ baseUrl is set in tsconfig.json but paths are not configured.\n` +
1550
+ ` TypeScript can resolve imports like 'src/utils', but webpack cannot.\n\n` +
1551
+ ` 💡 Solution 1: Add resolve.modules to webpack config:\n` +
1552
+ ` resolve: {\n` +
1553
+ ` modules: ['node_modules', '${tsconfig.compilerOptions.baseUrl}']\n` +
1554
+ ` }\n\n` +
1555
+ ` 💡 Solution 2: Add paths to tsconfig.json:\n` +
1556
+ ` "compilerOptions": {\n` +
1557
+ ` "baseUrl": ".",\n` +
1558
+ ` "paths": {\n` +
1559
+ ` "src/*": ["src/*"],\n` +
1560
+ ` "app/*": ["app/*"]\n` +
1561
+ ` }\n` +
1562
+ ` }\n`;
1563
+ }
1564
+ }
1565
+ catch (error) {
1566
+ // Ignore parse errors
1567
+ }
1568
+ }
1569
+ }
1570
+ // 4. Case-sensitivity issues
1571
+ const caseIssues = this.detectCaseSensitivityIssues(errors);
1572
+ if (caseIssues.length > 0) {
1573
+ otherIssues += `\n\n⚠️ Case-sensitivity issues detected!\n` +
1574
+ ` Your imports use different casing than the actual files:\n` +
1575
+ caseIssues.map(issue => ` - Imported: ${issue.expected}\n Actual: ${issue.actual}`).join('\n') +
1576
+ `\n\n 💡 Fix: Update your imports to match the exact file casing.\n` +
1577
+ ` This is especially important for Linux/CI environments where file systems are case-sensitive.\n`;
1578
+ }
1579
+ // Add other issues to error message
1580
+ if (otherIssues) {
1581
+ errorMessage += otherIssues;
1582
+ }
1082
1583
  // Add default suggestion if no other suggestions
1083
- if (!suggestion && !pathAliasSuggestion && !dependencySuggestion) {
1584
+ if (!suggestion && !pathAliasSuggestion && !dependencySuggestion && !otherIssues) {
1084
1585
  errorMessage += `\n💡 Fix the webpack build errors first, then regenerate stats.json:\n` +
1085
1586
  ` npx webpack --profile --json stats.json`;
1086
1587
  }