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.
- package/README.md +31 -32
- package/dist/analyzer/packlyze.d.ts +32 -0
- package/dist/analyzer/packlyze.d.ts.map +1 -1
- package/dist/analyzer/packlyze.js +628 -27
- package/dist/analyzer/packlyze.js.map +1 -1
- package/dist/cli.js +76 -3
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
//
|
|
1070
|
-
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
`
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
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
|
}
|