@sun-asterisk/sunlint 1.1.6 โ†’ 1.1.7

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/CHANGELOG.md CHANGED
@@ -1,3 +1,76 @@
1
+ # ๐ŸŽ‰ SunLint v1.1.7 Release Notes
2
+
3
+ **Release Date**: July 24, 2025
4
+ **Type**: Minor Release (ESLint Engine Enhancement & Smart Installation Guidance)
5
+
6
+ ---
7
+
8
+ ## ๐Ÿš€ **Key Improvements**
9
+
10
+ ### ๐Ÿง  **ESLint Engine Enhancement**
11
+ - **Enhanced**: ESLint v9+ flat config support with automatic legacy config conversion
12
+ - **Improved**: Dynamic plugin loading with availability detection (React, TypeScript, React Hooks)
13
+ - **Robust**: Better error handling and parsing error filtering for TypeScript files
14
+ - **Smart**: Temporary flat config generation for legacy compatibility
15
+
16
+ ### ๐ŸŽฏ **Smart Installation Guidance**
17
+ - **Intelligent**: Project type detection (NestJS, React, Next.js, Node.js)
18
+ - **Targeted**: Package manager detection (npm, yarn, pnpm) from package.json
19
+ - **Conditional**: Smart `--legacy-peer-deps` suggestion only when dependency conflicts detected
20
+ - **Clear**: Descriptive project-specific installation instructions
21
+
22
+ ### ๐Ÿ”ง **Project Type Detection**
23
+ - **NestJS Projects**: `pnpm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin`
24
+ - **React Projects**: `npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react eslint-plugin-react-hooks`
25
+ - **Conflict Detection**: Automatic detection of date-fns, React version conflicts, ESLint v8 issues
26
+
27
+ ### ๐Ÿ“ฆ **Dependency Management**
28
+ - **Aggregated Warnings**: Consolidated messages for missing plugins instead of spam
29
+ - **Graceful Fallback**: Analysis continues even with missing plugins, filtering parsing errors
30
+ - **Cleanup**: Automatic temporary config file cleanup after analysis
31
+
32
+ ---
33
+
34
+ ## ๐Ÿ›  **Technical Details**
35
+
36
+ ### **ESLint Integration**
37
+ - **Config Detection**: Automatic detection of flat config vs legacy config
38
+ - **Plugin Availability**: Runtime detection of React, TypeScript, React Hooks plugins
39
+ - **Parser Support**: Conditional TypeScript parser loading based on availability
40
+ - **Rule Filtering**: Skip rules for unavailable plugins with clear messaging
41
+
42
+ ### **Smart Guidance Logic**
43
+ - **Package Manager**: Detects preferred package manager from scripts and preinstall hooks
44
+ - **Conflict Detection**: Analyzes package.json for known dependency conflicts
45
+ - **Project Classification**: Distinguishes between frontend (React/Next.js) and backend (NestJS/Node.js) projects
46
+
47
+ ---
48
+
49
+ ## ๐Ÿ“‹ **Usage Examples**
50
+
51
+ ### **Minimal Installation (Works for basic analysis)**
52
+ ```bash
53
+ npm install --save-dev @sun-asterisk/sunlint
54
+ ```
55
+
56
+ ### **TypeScript Projects (Recommended)**
57
+ ```bash
58
+ npm install --save-dev @sun-asterisk/sunlint typescript
59
+ ```
60
+
61
+ ### **Full Installation (All project types)**
62
+ ```bash
63
+ npm install --save-dev @sun-asterisk/sunlint eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react eslint-plugin-react-hooks typescript
64
+ ```
65
+
66
+ ---
67
+
68
+ ## ๐ŸŽ‰ **What's Next**
69
+
70
+ SunLint v1.1.7 makes ESLint integration more robust and user-friendly with intelligent project detection and clear installation guidance. No more guessing what dependencies to install! ๐Ÿš€
71
+
72
+ ---
73
+
1
74
  # ๐ŸŽ‰ SunLint v1.1.0 Release Notes
2
75
 
3
76
  **Release Date**: July 23, 2025
@@ -186,25 +186,24 @@ class ESLintEngine extends AnalysisEngineInterface {
186
186
  // Use flat config (ESLint v9+ preferred)
187
187
  eslintOptions = {
188
188
  overrideConfigFile: null, // Let ESLint find flat config automatically
189
- overrideConfig: this.createBaseConfig(),
190
189
  fix: this.config?.fix || false,
191
190
  cache: this.config?.cache || false,
192
191
  cwd: projectPath
193
192
  };
194
193
  console.log(`โœ… [ESLintEngine] Using flat config (modern ESLint v9+)`);
195
194
  } else if (configDetection.hasLegacyConfig || configDetection.hasPackageConfig) {
196
- // Use legacy config for compatibility - but ESLint v9+ only supports flat config
197
- // We'll convert legacy to flat config on-the-fly
195
+ // ESLint v9+ requires flat config - convert legacy config to flat config format
196
+ const flatConfig = await this.convertLegacyToFlatConfig(projectPath, configDetection);
198
197
  eslintOptions = {
199
- overrideConfigFile: null, // Force flat config mode
200
- overrideConfig: this.createBaseConfig(),
198
+ overrideConfigFile: null, // Use our generated flat config
199
+ overrideConfig: flatConfig,
201
200
  fix: this.config?.fix || false,
202
201
  cache: this.config?.cache || false,
203
202
  cwd: projectPath
204
203
  };
205
- console.log(`โœ… [ESLintEngine] Legacy config detected, but using flat config mode (ESLint v9+ requirement)`);
204
+ console.log(`โœ… [ESLintEngine] Legacy config converted to flat config for ESLint v9+ compatibility`);
206
205
  } else {
207
- // No config found - use SunLint's base config only, create empty config file
206
+ // No config found - use SunLint's base config only
208
207
  eslintOptions = {
209
208
  overrideConfig: this.createBaseConfig(),
210
209
  fix: this.config?.fix || false,
@@ -228,6 +227,310 @@ class ESLintEngine extends AnalysisEngineInterface {
228
227
  }
229
228
  }
230
229
 
230
+ /**
231
+ * Extract rules array from eslint config
232
+ * @param {Object} eslintConfig - ESLint config object
233
+ * @returns {Array} Rules array for plugin detection
234
+ */
235
+ extractRulesFromConfig(eslintConfig) {
236
+ // Convert rules object keys back to rule objects for plugin detection
237
+ const rules = [];
238
+ for (const ruleKey of Object.keys(eslintConfig.rules || {})) {
239
+ if (ruleKey.startsWith('custom/typescript_s')) {
240
+ rules.push({ id: ruleKey.replace('custom/typescript_', '').toUpperCase() });
241
+ } else if (ruleKey.startsWith('custom/')) {
242
+ rules.push({ id: ruleKey.replace('custom/', '').toUpperCase() });
243
+ } else if (ruleKey.startsWith('react/')) {
244
+ rules.push({ id: 'R001' }); // Mock React rule for detection
245
+ } else if (ruleKey.includes('@typescript-eslint/')) {
246
+ rules.push({ id: 'T001' }); // Mock TypeScript rule for detection
247
+ }
248
+ }
249
+ return rules;
250
+ }
251
+
252
+ /**
253
+ * Create temporary flat config file for legacy compatibility
254
+ * Following Rule C006: Verb-noun naming
255
+ * @param {string} projectPath - Path to the project
256
+ * @param {Object} configDetection - Config detection results
257
+ * @param {Object} eslintConfig - Analysis config to merge
258
+ * @returns {Promise<string>} Path to temporary flat config file
259
+ */
260
+ async createTemporaryFlatConfig(projectPath, configDetection, eslintConfig) {
261
+ const fs = require('fs');
262
+ const path = require('path');
263
+
264
+ try {
265
+ let baseConfig;
266
+
267
+ if (configDetection.hasFlatConfig) {
268
+ // Load existing flat config
269
+ const existingConfigPath = path.join(projectPath, 'eslint.config.js');
270
+ if (fs.existsSync(existingConfigPath)) {
271
+ try {
272
+ // Read and parse existing flat config
273
+ const configContent = fs.readFileSync(existingConfigPath, 'utf8');
274
+ // For now, use a simple base config - parsing dynamic imports is complex
275
+ baseConfig = {
276
+ files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'],
277
+ languageOptions: {
278
+ ecmaVersion: 'latest',
279
+ sourceType: 'module',
280
+ parserOptions: {
281
+ ecmaFeatures: {
282
+ jsx: true
283
+ }
284
+ }
285
+ },
286
+ rules: {}
287
+ };
288
+ } catch (error) {
289
+ console.warn(`โš ๏ธ [ESLintEngine] Failed to parse existing flat config: ${error.message}`);
290
+ baseConfig = this.createBaseConfig();
291
+ }
292
+ } else {
293
+ baseConfig = this.createBaseConfig();
294
+ }
295
+ } else {
296
+ // Convert legacy config
297
+ baseConfig = await this.convertLegacyToFlatConfig(projectPath, configDetection);
298
+ }
299
+
300
+ // Build plugin imports based on what's needed AND what's available
301
+ const rules = this.extractRulesFromConfig(eslintConfig);
302
+ const needsReact = this.needsReactPlugins(rules);
303
+ const needsTypeScript = this.needsTypeScriptPlugins(rules);
304
+
305
+ // Check plugin availability in target project
306
+ const hasReact = needsReact && this.isReactPluginAvailable(projectPath);
307
+ const hasReactHooks = needsReact && this.isReactHooksPluginAvailable(projectPath);
308
+ const hasTypeScript = needsTypeScript && this.isTypeScriptPluginAvailable(projectPath);
309
+ const hasTypeScriptParser = this.isTypeScriptParserAvailable(projectPath);
310
+
311
+ let pluginImports = '';
312
+ let pluginDefs = '{ "custom": customPlugin';
313
+
314
+ if (hasReact) {
315
+ pluginImports += `\nimport reactPlugin from 'eslint-plugin-react';`;
316
+ pluginDefs += ', "react": reactPlugin';
317
+ }
318
+
319
+ if (hasReactHooks) {
320
+ pluginImports += `\nimport reactHooksPlugin from 'eslint-plugin-react-hooks';`;
321
+ pluginDefs += ', "react-hooks": reactHooksPlugin';
322
+ }
323
+
324
+ if (hasTypeScript) {
325
+ pluginImports += `\nimport typescriptPlugin from '@typescript-eslint/eslint-plugin';`;
326
+ pluginDefs += ', "@typescript-eslint": typescriptPlugin';
327
+ }
328
+
329
+ pluginDefs += ' }';
330
+
331
+ // Filter rules to only include those for available plugins
332
+ const filteredRules = {};
333
+ const skippedRules = { react: [], reactHooks: [], typescript: [] };
334
+
335
+ for (const [ruleKey, ruleConfig] of Object.entries(eslintConfig.rules || {})) {
336
+ if (ruleKey.startsWith('react/') && !hasReact) {
337
+ skippedRules.react.push(ruleKey);
338
+ continue;
339
+ }
340
+ if (ruleKey.startsWith('react-hooks/') && !hasReactHooks) {
341
+ skippedRules.reactHooks.push(ruleKey);
342
+ continue;
343
+ }
344
+ if (ruleKey.startsWith('@typescript-eslint/') && !hasTypeScript) {
345
+ skippedRules.typescript.push(ruleKey);
346
+ continue;
347
+ }
348
+ filteredRules[ruleKey] = ruleConfig;
349
+ }
350
+
351
+ // Summary of skipped rules instead of individual warnings
352
+ if (skippedRules.react.length > 0) {
353
+ console.warn(`โš ๏ธ [ESLintEngine] Skipped ${skippedRules.react.length} React rules - plugin not available`);
354
+ }
355
+ if (skippedRules.reactHooks.length > 0) {
356
+ console.warn(`โš ๏ธ [ESLintEngine] Skipped ${skippedRules.reactHooks.length} React Hooks rules - plugin not available`);
357
+ }
358
+ if (skippedRules.typescript.length > 0) {
359
+ console.warn(`โš ๏ธ [ESLintEngine] Skipped ${skippedRules.typescript.length} TypeScript ESLint rules - plugin not available`);
360
+ }
361
+
362
+ // Merge with analysis config using filtered rules
363
+ const mergedConfig = {
364
+ ...baseConfig,
365
+ ...eslintConfig,
366
+ rules: {
367
+ ...baseConfig.rules,
368
+ ...filteredRules
369
+ }
370
+ };
371
+
372
+ // Create temporary config file in project directory
373
+ const tempConfigPath = path.join(projectPath, '.sunlint-eslint.config.js');
374
+
375
+ // Create simple config compatible with flat config format
376
+ const configForExport = {
377
+ files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'],
378
+ languageOptions: {
379
+ ecmaVersion: 'latest',
380
+ sourceType: 'module',
381
+ parserOptions: {
382
+ ecmaFeatures: {
383
+ jsx: true
384
+ }
385
+ }
386
+ },
387
+ rules: {
388
+ ...baseConfig.rules,
389
+ ...filteredRules
390
+ }
391
+ };
392
+
393
+ const configContent = `// Temporary flat config generated by SunLint
394
+ import customPlugin from '${path.resolve(__dirname, '../integrations/eslint/plugin/index.js')}';${pluginImports}
395
+
396
+ export default [
397
+ ${JSON.stringify(configForExport, null, 2).replace('"rules":', `"plugins": ${pluginDefs},\n "rules":`)},
398
+ {
399
+ files: ['**/*.ts', '**/*.tsx'],
400
+ plugins: ${pluginDefs},
401
+ languageOptions: {${hasTypeScriptParser ? `
402
+ parser: (await import('@typescript-eslint/parser')).default,` : ''}
403
+ ecmaVersion: 'latest',
404
+ sourceType: 'module',
405
+ parserOptions: {
406
+ ecmaFeatures: {
407
+ jsx: true
408
+ }
409
+ }
410
+ },
411
+ rules: ${JSON.stringify({...baseConfig.rules, ...filteredRules}, null, 2)}
412
+ }
413
+ ];
414
+ `;
415
+
416
+ fs.writeFileSync(tempConfigPath, configContent);
417
+ console.log(`๐Ÿ”ง [ESLintEngine] Created temporary flat config: ${tempConfigPath}`);
418
+
419
+ // Schedule cleanup
420
+ this.tempConfigPaths = this.tempConfigPaths || [];
421
+ this.tempConfigPaths.push(tempConfigPath);
422
+
423
+ return tempConfigPath;
424
+ } catch (error) {
425
+ console.warn(`โš ๏ธ [ESLintEngine] Failed to create temporary flat config: ${error.message}`);
426
+ throw error;
427
+ }
428
+ }
429
+
430
+ /**
431
+ * Convert legacy ESLint config to flat config format
432
+ * Following Rule C006: Verb-noun naming
433
+ * @param {string} projectPath - Path to the project
434
+ * @param {Object} configDetection - Config detection results
435
+ * @returns {Promise<Object>} Flat config object
436
+ */
437
+ async convertLegacyToFlatConfig(projectPath, configDetection) {
438
+ const fs = require('fs');
439
+ const path = require('path');
440
+
441
+ let legacyConfig = {};
442
+
443
+ try {
444
+ // Load legacy config from .eslintrc.json
445
+ if (configDetection.foundFiles.includes('.eslintrc.json')) {
446
+ const configPath = path.join(projectPath, '.eslintrc.json');
447
+ const configContent = fs.readFileSync(configPath, 'utf8');
448
+ legacyConfig = JSON.parse(configContent);
449
+ }
450
+
451
+ // Convert to flat config format
452
+ const flatConfig = {
453
+ files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'],
454
+ languageOptions: {
455
+ ecmaVersion: legacyConfig.env?.es2022 ? 2022 :
456
+ legacyConfig.env?.es2021 ? 2021 :
457
+ legacyConfig.env?.es6 ? 6 : 'latest',
458
+ sourceType: legacyConfig.parserOptions?.sourceType || 'module',
459
+ globals: {}
460
+ },
461
+ plugins: {},
462
+ rules: legacyConfig.rules || {}
463
+ };
464
+
465
+ // Convert env to globals
466
+ if (legacyConfig.env) {
467
+ if (legacyConfig.env.browser) {
468
+ Object.assign(flatConfig.languageOptions.globals, {
469
+ window: 'readonly',
470
+ document: 'readonly',
471
+ navigator: 'readonly',
472
+ console: 'readonly'
473
+ });
474
+ }
475
+ if (legacyConfig.env.node) {
476
+ Object.assign(flatConfig.languageOptions.globals, {
477
+ process: 'readonly',
478
+ Buffer: 'readonly',
479
+ __dirname: 'readonly',
480
+ __filename: 'readonly',
481
+ module: 'readonly',
482
+ require: 'readonly',
483
+ exports: 'readonly',
484
+ global: 'readonly'
485
+ });
486
+ }
487
+ if (legacyConfig.env.es6) {
488
+ Object.assign(flatConfig.languageOptions.globals, {
489
+ Promise: 'readonly',
490
+ Set: 'readonly',
491
+ Map: 'readonly'
492
+ });
493
+ }
494
+ }
495
+
496
+ // Set parser if specified
497
+ if (legacyConfig.parser) {
498
+ if (legacyConfig.parser === '@typescript-eslint/parser') {
499
+ flatConfig.languageOptions.parser = this.loadTypeScriptParser();
500
+ }
501
+ }
502
+
503
+ // Convert parser options
504
+ if (legacyConfig.parserOptions) {
505
+ flatConfig.languageOptions.parserOptions = legacyConfig.parserOptions;
506
+ }
507
+
508
+ // Handle extends - merge base rules
509
+ if (legacyConfig.extends) {
510
+ const extendsList = Array.isArray(legacyConfig.extends) ? legacyConfig.extends : [legacyConfig.extends];
511
+
512
+ for (const extend of extendsList) {
513
+ if (extend === 'eslint:recommended') {
514
+ // Add some basic recommended rules
515
+ Object.assign(flatConfig.rules, {
516
+ 'no-unused-vars': 'warn',
517
+ 'no-undef': 'error',
518
+ 'no-console': 'warn'
519
+ });
520
+ }
521
+ }
522
+ }
523
+
524
+ console.log(`๐Ÿ”„ [ESLintEngine] Converted legacy config to flat config`);
525
+ return flatConfig;
526
+
527
+ } catch (error) {
528
+ console.warn(`โš ๏ธ [ESLintEngine] Failed to convert legacy config: ${error.message}`);
529
+ // Fallback to base config
530
+ return this.createBaseConfig();
531
+ }
532
+ }
533
+
231
534
  /**
232
535
  * Create base ESLint configuration
233
536
  * Following Rule C006: Verb-noun naming
@@ -303,6 +606,298 @@ class ESLintEngine extends AnalysisEngineInterface {
303
606
  console.warn('โš ๏ธ Using default ESLint rule mapping');
304
607
  }
305
608
 
609
+ /**
610
+ * Detect project type from package.json and file patterns
611
+ * @param {string} projectPath - Project path
612
+ * @param {string[]} files - Files being analyzed
613
+ * @returns {Object} Project type information
614
+ */
615
+ detectProjectType(projectPath, files) {
616
+ const fs = require('fs');
617
+ const path = require('path');
618
+
619
+ const result = {
620
+ isReactProject: false,
621
+ isNextProject: false,
622
+ isNestProject: false,
623
+ isNodeProject: false,
624
+ hasReactFiles: false,
625
+ hasNestFiles: false,
626
+ packageManager: 'npm'
627
+ };
628
+
629
+ try {
630
+ // Check package.json for project type indicators
631
+ const packageJsonPath = path.join(projectPath, 'package.json');
632
+ if (fs.existsSync(packageJsonPath)) {
633
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
634
+
635
+ // Check dependencies for project type
636
+ const allDeps = {
637
+ ...packageJson.dependencies,
638
+ ...packageJson.devDependencies,
639
+ ...packageJson.peerDependencies
640
+ };
641
+
642
+ if (allDeps.react || allDeps['@types/react']) {
643
+ result.isReactProject = true;
644
+ }
645
+
646
+ if (allDeps.next || allDeps['@types/next']) {
647
+ result.isNextProject = true;
648
+ }
649
+
650
+ if (allDeps['@nestjs/core'] || allDeps['@nestjs/common']) {
651
+ result.isNestProject = true;
652
+ }
653
+
654
+ // Check package manager from scripts
655
+ if (packageJson.scripts && Object.values(packageJson.scripts).some(script => script.includes('pnpm'))) {
656
+ result.packageManager = 'pnpm';
657
+ } else if (packageJson.scripts && Object.values(packageJson.scripts).some(script => script.includes('yarn'))) {
658
+ result.packageManager = 'yarn';
659
+ }
660
+
661
+ // Check for preinstall script indicating package manager preference
662
+ if (packageJson.scripts?.preinstall?.includes('pnpm')) {
663
+ result.packageManager = 'pnpm';
664
+ } else if (packageJson.scripts?.preinstall?.includes('yarn')) {
665
+ result.packageManager = 'yarn';
666
+ }
667
+ }
668
+
669
+ // Check file patterns
670
+ const hasJsxTsx = files.some(file => {
671
+ const ext = path.extname(file).toLowerCase();
672
+ return ['.jsx', '.tsx'].includes(ext);
673
+ });
674
+
675
+ const hasNestFiles = files.some(file => {
676
+ return file.includes('controller.ts') ||
677
+ file.includes('service.ts') ||
678
+ file.includes('module.ts') ||
679
+ file.includes('main.ts');
680
+ });
681
+
682
+ result.hasReactFiles = hasJsxTsx && !result.isNestProject;
683
+ result.hasNestFiles = hasNestFiles;
684
+ result.isNodeProject = !result.isReactProject && !result.isNextProject;
685
+
686
+ } catch (error) {
687
+ console.warn(`โš ๏ธ [ESLintEngine] Failed to detect project type: ${error.message}`);
688
+ }
689
+
690
+ return result;
691
+ }
692
+
693
+ /**
694
+ * Check if project has dependency conflicts that require --legacy-peer-deps
695
+ * @param {string} projectPath - Project path
696
+ * @returns {boolean} True if project has known dependency conflicts
697
+ */
698
+ hasKnownDependencyConflicts(projectPath) {
699
+ const fs = require('fs');
700
+ const path = require('path');
701
+
702
+ try {
703
+ const packageJsonPath = path.join(projectPath, 'package.json');
704
+ if (!fs.existsSync(packageJsonPath)) {
705
+ return false;
706
+ }
707
+
708
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
709
+ const allDeps = {
710
+ ...packageJson.dependencies,
711
+ ...packageJson.devDependencies,
712
+ ...packageJson.peerDependencies
713
+ };
714
+
715
+ // Check for known problematic combinations
716
+ const conflicts = [
717
+ // date-fns version conflicts
718
+ () => {
719
+ const dateFns = allDeps['date-fns'];
720
+ const dateFnsTz = allDeps['date-fns-tz'];
721
+ if (dateFns && dateFnsTz) {
722
+ // If date-fns is v2.x and date-fns-tz is v3.x, there's likely a conflict
723
+ if (dateFns.includes('2.') && dateFnsTz.includes('3.')) {
724
+ return true;
725
+ }
726
+ }
727
+ return false;
728
+ },
729
+
730
+ // React version conflicts
731
+ () => {
732
+ const react = allDeps['react'];
733
+ const reactDom = allDeps['react-dom'];
734
+ if (react && reactDom) {
735
+ // Check for major version mismatches
736
+ const reactMajor = react.match(/(\d+)\./)?.[1];
737
+ const reactDomMajor = reactDom.match(/(\d+)\./)?.[1];
738
+ if (reactMajor && reactDomMajor && reactMajor !== reactDomMajor) {
739
+ return true;
740
+ }
741
+ }
742
+ return false;
743
+ },
744
+
745
+ // ESLint version conflicts (common with older projects)
746
+ () => {
747
+ const eslint = allDeps['eslint'];
748
+ if (eslint && eslint.includes('8.')) {
749
+ // ESLint v8 with newer plugins often has peer dependency issues
750
+ return true;
751
+ }
752
+ return false;
753
+ }
754
+ ];
755
+
756
+ return conflicts.some(check => check());
757
+
758
+ } catch (error) {
759
+ // If we can't read package.json, assume no conflicts
760
+ return false;
761
+ }
762
+ }
763
+
764
+ /**
765
+ * Provide appropriate installation guidance based on project type
766
+ * @param {Object} projectType - Project type information
767
+ * @param {number} tsFileCount - Number of TypeScript files
768
+ * @param {number} reactFileCount - Number of React files
769
+ * @param {boolean} hasTypeScriptParser - TypeScript parser availability
770
+ * @param {boolean} hasReactPlugin - React plugin availability
771
+ * @param {boolean} hasReactHooksPlugin - React Hooks plugin availability
772
+ * @param {string} projectPath - Project path for conflict detection
773
+ */
774
+ provideInstallationGuidance(projectType, tsFileCount, reactFileCount, hasTypeScriptParser, hasReactPlugin, hasReactHooksPlugin, projectPath) {
775
+ const missingDeps = [];
776
+ const projectDescription = this.getProjectDescription(projectType, tsFileCount, reactFileCount);
777
+
778
+ // TypeScript dependencies (needed for most projects with .ts files)
779
+ if (tsFileCount > 0 && !hasTypeScriptParser) {
780
+ missingDeps.push('@typescript-eslint/parser', '@typescript-eslint/eslint-plugin');
781
+ }
782
+
783
+ // React dependencies (only for actual React projects, not NestJS)
784
+ if (projectType.hasReactFiles && !projectType.isNestProject) {
785
+ if (!hasReactPlugin) missingDeps.push('eslint-plugin-react');
786
+ if (!hasReactHooksPlugin) missingDeps.push('eslint-plugin-react-hooks');
787
+ }
788
+
789
+ if (missingDeps.length > 0) {
790
+ console.log(`\n๐Ÿ“ฆ [SunLint] To enable full analysis of your ${projectDescription}, install:`);
791
+
792
+ // Use appropriate package manager and flags
793
+ const packageManager = projectType.packageManager;
794
+ const installFlag = packageManager === 'npm' ? '--save-dev' : packageManager === 'yarn' ? '--dev' : '--save-dev';
795
+
796
+ // Only suggest --legacy-peer-deps if the project has known dependency conflicts
797
+ let legacyFlag = '';
798
+ if (packageManager === 'npm' && this.hasKnownDependencyConflicts(projectPath)) {
799
+ legacyFlag = ' --legacy-peer-deps';
800
+ console.log(` โš ๏ธ Detected dependency conflicts in your project.`);
801
+ }
802
+
803
+ console.log(` ${packageManager} install ${installFlag} ${missingDeps.join(' ')}${legacyFlag}`);
804
+ console.log(` Then SunLint will analyze all files with full ${this.getToolDescription(missingDeps)} support.\n`);
805
+ }
806
+ }
807
+
808
+ /**
809
+ * Get project description for user guidance
810
+ * @param {Object} projectType - Project type information
811
+ * @param {number} tsFileCount - Number of TypeScript files
812
+ * @param {number} reactFileCount - Number of React files
813
+ * @returns {string} Project description
814
+ */
815
+ getProjectDescription(projectType, tsFileCount, reactFileCount) {
816
+ if (projectType.isNestProject) {
817
+ return `${tsFileCount} TypeScript files (NestJS backend)`;
818
+ } else if (projectType.isNextProject) {
819
+ return `${tsFileCount} TypeScript and ${reactFileCount} React files (Next.js project)`;
820
+ } else if (projectType.isReactProject) {
821
+ return `${tsFileCount} TypeScript and ${reactFileCount} React files (React project)`;
822
+ } else if (tsFileCount > 0) {
823
+ return `${tsFileCount} TypeScript files (Node.js project)`;
824
+ } else {
825
+ return 'JavaScript files';
826
+ }
827
+ }
828
+
829
+ /**
830
+ * Get tool description for user guidance
831
+ * @param {string[]} missingDeps - Missing dependencies
832
+ * @returns {string} Tool description
833
+ */
834
+ getToolDescription(missingDeps) {
835
+ const tools = [];
836
+ if (missingDeps.some(dep => dep.includes('typescript-eslint'))) {
837
+ tools.push('TypeScript');
838
+ }
839
+ if (missingDeps.some(dep => dep.includes('react'))) {
840
+ tools.push('React');
841
+ }
842
+ return tools.join(' and ');
843
+ }
844
+
845
+ /**
846
+ * Check if React plugin is available in project
847
+ * @param {string} projectPath - Project path to check
848
+ * @returns {boolean} True if React plugin is available
849
+ */
850
+ isReactPluginAvailable(projectPath) {
851
+ try {
852
+ require.resolve('eslint-plugin-react', { paths: [projectPath] });
853
+ return true;
854
+ } catch (error) {
855
+ return false;
856
+ }
857
+ }
858
+
859
+ /**
860
+ * Check if React Hooks plugin is available in project
861
+ * @param {string} projectPath - Project path to check
862
+ * @returns {boolean} True if React Hooks plugin is available
863
+ */
864
+ isReactHooksPluginAvailable(projectPath) {
865
+ try {
866
+ require.resolve('eslint-plugin-react-hooks', { paths: [projectPath] });
867
+ return true;
868
+ } catch (error) {
869
+ return false;
870
+ }
871
+ }
872
+
873
+ /**
874
+ * Check if TypeScript plugin is available in project
875
+ * @param {string} projectPath - Project path to check
876
+ * @returns {boolean} True if TypeScript plugin is available
877
+ */
878
+ isTypeScriptPluginAvailable(projectPath) {
879
+ try {
880
+ require.resolve('@typescript-eslint/eslint-plugin', { paths: [projectPath] });
881
+ return true;
882
+ } catch (error) {
883
+ return false;
884
+ }
885
+ }
886
+
887
+ /**
888
+ * Check if TypeScript parser is available in project
889
+ * @param {string} projectPath - Project path to check
890
+ * @returns {boolean} True if TypeScript parser is available
891
+ */
892
+ isTypeScriptParserAvailable(projectPath) {
893
+ try {
894
+ require.resolve('@typescript-eslint/parser', { paths: [projectPath] });
895
+ return true;
896
+ } catch (error) {
897
+ return false;
898
+ }
899
+ }
900
+
306
901
  /**
307
902
  * Load React ESLint plugin
308
903
  * Following Rule C006: Verb-noun naming
@@ -433,7 +1028,7 @@ class ESLintEngine extends AnalysisEngineInterface {
433
1028
 
434
1029
  try {
435
1030
  // Filter files for JS/TS only
436
- const jstsFiles = files.filter(file => this.isJavaScriptTypeScriptFile(file));
1031
+ let jstsFiles = files.filter(file => this.isJavaScriptTypeScriptFile(file));
437
1032
 
438
1033
  if (jstsFiles.length === 0) {
439
1034
  console.warn('โš ๏ธ No JavaScript/TypeScript files found for ESLint analysis');
@@ -448,44 +1043,109 @@ class ESLintEngine extends AnalysisEngineInterface {
448
1043
  return results;
449
1044
  }
450
1045
 
451
- // Reconfigure ESLint with analysis-specific rules and detect project config
1046
+ // Find project root from input path (usually the project's working directory)
452
1047
  const path = require('path');
453
- const projectPath = path.dirname(jstsFiles[0]) || process.cwd();
1048
+ let projectPath;
454
1049
 
455
- const eslintInstance = await this.createESLintInstance(projectPath);
1050
+ if (options.input) {
1051
+ // If input is specified, find project root from it
1052
+ const inputPath = path.resolve(options.input);
1053
+ // Always go up to find project root, not use input directory directly
1054
+ projectPath = this.findProjectRoot([inputPath]);
1055
+ } else if (jstsFiles.length > 0) {
1056
+ // Find project root from all files
1057
+ projectPath = this.findProjectRoot(jstsFiles);
1058
+ } else {
1059
+ projectPath = process.cwd();
1060
+ }
456
1061
 
457
- // Override config for analysis-specific rules
458
- const analysisConfig = {
459
- ...this.createBaseConfig(),
460
- ...eslintConfig
461
- };
1062
+ console.log(`๐Ÿ” [ESLintEngine] Using project path: ${projectPath}`);
462
1063
 
463
- // Re-create instance with analysis config
464
- const { ESLint } = await this.loadESLint();
1064
+ // Get config detection for reuse
465
1065
  const configDetection = this.detectESLintConfig(projectPath);
1066
+
1067
+ // Check for missing dependencies and provide installation guidance
1068
+ const hasTypeScriptParser = this.isTypeScriptParserAvailable(projectPath);
1069
+ const hasReactPlugin = this.isReactPluginAvailable(projectPath);
1070
+ const hasReactHooksPlugin = this.isReactHooksPluginAvailable(projectPath);
1071
+ const hasTypeScriptPlugin = this.isTypeScriptPluginAvailable(projectPath);
466
1072
 
1073
+ // Detect project type from package.json and file patterns
1074
+ const projectType = this.detectProjectType(projectPath, jstsFiles);
1075
+
1076
+ // Count TypeScript files to determine if we need to recommend TypeScript tools
1077
+ const tsFileCount = jstsFiles.filter(file => {
1078
+ const ext = path.extname(file).toLowerCase();
1079
+ return ['.ts', '.tsx'].includes(ext);
1080
+ }).length;
1081
+
1082
+ // Count React-like files to determine if we need React tools
1083
+ const reactFileCount = jstsFiles.filter(file => {
1084
+ const ext = path.extname(file).toLowerCase();
1085
+ return ['.jsx', '.tsx'].includes(ext);
1086
+ }).length;
1087
+
1088
+ // Provide helpful installation guidance based on project type
1089
+ this.provideInstallationGuidance(projectType, tsFileCount, reactFileCount, hasTypeScriptParser, hasReactPlugin, hasReactHooksPlugin, projectPath);
1090
+
1091
+ // Create ESLint instance with proper config
1092
+ const { ESLint } = await this.loadESLint();
467
1093
  let finalESLintOptions;
1094
+
1095
+ // Configure ESLint to handle files appropriately
468
1096
  if (configDetection.hasFlatConfig) {
1097
+ // For flat config, always create temporary config to ensure plugin compatibility
1098
+ const tempFlatConfigPath = await this.createTemporaryFlatConfig(projectPath, configDetection, eslintConfig);
469
1099
  finalESLintOptions = {
470
- overrideConfigFile: null,
471
- overrideConfig: analysisConfig,
1100
+ overrideConfigFile: tempFlatConfigPath,
472
1101
  cwd: projectPath
473
1102
  };
1103
+ console.log(`โœ… [ESLintEngine] Created temporary flat config for plugin compatibility`);
1104
+ } else if (configDetection.hasLegacyConfig || configDetection.hasPackageConfig) {
1105
+ // For legacy config, create a temporary flat config file
1106
+ const tempFlatConfigPath = await this.createTemporaryFlatConfig(projectPath, configDetection, eslintConfig);
1107
+ finalESLintOptions = {
1108
+ overrideConfigFile: tempFlatConfigPath,
1109
+ cwd: projectPath
1110
+ };
1111
+ console.log(`โœ… [ESLintEngine] Created temporary flat config for legacy compatibility`);
474
1112
  } else {
475
- // For legacy config or no config, omit overrideConfigFile to use default behavior
1113
+ // No config found - use analysis config only
476
1114
  finalESLintOptions = {
477
- overrideConfig: analysisConfig,
1115
+ overrideConfig: eslintConfig,
478
1116
  cwd: projectPath
479
1117
  };
1118
+ console.log(`โš ๏ธ [ESLintEngine] Using analysis config only`);
480
1119
  }
481
1120
 
482
1121
  const finalESLintInstance = new ESLint(finalESLintOptions);
483
1122
 
484
- // Run ESLint analysis
1123
+ // Run ESLint analysis - let ESLint handle parsing errors gracefully
1124
+ console.log(`๐Ÿ” [ESLintEngine] Analyzing ${jstsFiles.length} JavaScript/TypeScript files...`);
485
1125
  const eslintResults = await finalESLintInstance.lintFiles(jstsFiles);
486
1126
 
1127
+ // Filter out parsing errors when TypeScript parser is not available
1128
+ let processedResults = eslintResults;
1129
+ if (!hasTypeScriptParser) {
1130
+ let parsingErrorCount = 0;
1131
+ processedResults = eslintResults.map(result => {
1132
+ const filteredMessages = result.messages.filter(message => {
1133
+ if (message.ruleId === null && message.message.includes('Parsing error')) {
1134
+ parsingErrorCount++;
1135
+ return false; // Skip parsing errors
1136
+ }
1137
+ return true; // Keep all other messages
1138
+ });
1139
+ return { ...result, messages: filteredMessages };
1140
+ });
1141
+
1142
+ if (parsingErrorCount > 0) {
1143
+ console.log(`โ„น๏ธ [ESLintEngine] Filtered ${parsingErrorCount} TypeScript parsing errors (install @typescript-eslint/parser for full TypeScript support)`);
1144
+ }
1145
+ }
1146
+
487
1147
  // Convert ESLint results to SunLint format
488
- results.results = this.convertESLintResults(eslintResults, rules);
1148
+ results.results = this.convertESLintResults(processedResults, rules);
489
1149
  results.filesAnalyzed = jstsFiles.length;
490
1150
  results.metadata.rulesAnalyzed = rules.map(r => r.id);
491
1151
  results.metadata.eslintRulesUsed = Object.keys(eslintConfig.rules);
@@ -498,6 +1158,76 @@ class ESLintEngine extends AnalysisEngineInterface {
498
1158
  return results;
499
1159
  }
500
1160
 
1161
+ /**
1162
+ * Find project root from a list of files or a directory
1163
+ * Following Rule C006: Verb-noun naming
1164
+ * @param {string[]} paths - List of file paths or directories
1165
+ * @returns {string} Project root path
1166
+ */
1167
+ findProjectRoot(paths) {
1168
+ const path = require('path');
1169
+
1170
+ if (paths.length === 0) {
1171
+ return process.cwd();
1172
+ }
1173
+
1174
+ // Start from the first path (could be directory or file)
1175
+ let startPath = paths[0];
1176
+
1177
+ // If it's a file, get its directory
1178
+ if (fs.existsSync(startPath) && fs.statSync(startPath).isFile()) {
1179
+ startPath = path.dirname(startPath);
1180
+ }
1181
+
1182
+ // Look for project indicators going up the tree from start path
1183
+ let currentPath = path.resolve(startPath);
1184
+ while (currentPath !== path.dirname(currentPath)) { // Stop at root
1185
+ const packageJsonPath = path.join(currentPath, 'package.json');
1186
+ const eslintConfigPath = path.join(currentPath, 'eslint.config.js');
1187
+ const eslintrcPath = path.join(currentPath, '.eslintrc.json');
1188
+ const tsConfigPath = path.join(currentPath, 'tsconfig.json');
1189
+
1190
+ // Found project root indicators
1191
+ if (fs.existsSync(packageJsonPath) || fs.existsSync(eslintConfigPath) ||
1192
+ fs.existsSync(eslintrcPath) || fs.existsSync(tsConfigPath)) {
1193
+ return currentPath;
1194
+ }
1195
+
1196
+ // Go up one level
1197
+ currentPath = path.dirname(currentPath);
1198
+ }
1199
+
1200
+ // If nothing found, return the original start path
1201
+ return path.resolve(startPath);
1202
+ }
1203
+
1204
+ /**
1205
+ * Find common path between two paths
1206
+ * Following Rule C006: Verb-noun naming
1207
+ * @param {string} path1 - First path
1208
+ * @param {string} path2 - Second path
1209
+ * @returns {string} Common path
1210
+ */
1211
+ findCommonPath(path1, path2) {
1212
+ const path = require('path');
1213
+
1214
+ const parts1 = path1.split(path.sep);
1215
+ const parts2 = path2.split(path.sep);
1216
+
1217
+ const commonParts = [];
1218
+ const minLength = Math.min(parts1.length, parts2.length);
1219
+
1220
+ for (let i = 0; i < minLength; i++) {
1221
+ if (parts1[i] === parts2[i]) {
1222
+ commonParts.push(parts1[i]);
1223
+ } else {
1224
+ break;
1225
+ }
1226
+ }
1227
+
1228
+ return commonParts.join(path.sep) || path.sep;
1229
+ }
1230
+
501
1231
  /**
502
1232
  * Check if file is JavaScript or TypeScript
503
1233
  * Following Rule C006: Verb-noun naming
@@ -595,6 +1325,37 @@ class ESLintEngine extends AnalysisEngineInterface {
595
1325
 
596
1326
  // Map SunLint rules to ESLint rules
597
1327
  for (const rule of rules) {
1328
+ // For Security rules, always use custom plugin (ignore mapping file)
1329
+ if (rule.id.startsWith('S')) {
1330
+ const customRuleName = `custom/typescript_${rule.id.toLowerCase()}`;
1331
+ const ruleConfig = this.mapSeverity(rule.severity || 'warning');
1332
+ config.rules[customRuleName] = ruleConfig;
1333
+ continue;
1334
+ }
1335
+
1336
+ // For Common rules (C series), use rule ID directly in custom plugin
1337
+ if (rule.id.startsWith('C')) {
1338
+ const customRuleName = `custom/${rule.id.toLowerCase()}`;
1339
+ const ruleConfig = this.mapSeverity(rule.severity || 'warning');
1340
+
1341
+ // Add rule configuration for specific rules
1342
+ if (rule.id === 'C010') {
1343
+ config.rules[customRuleName] = [ruleConfig, { maxDepth: 3 }];
1344
+ } else {
1345
+ config.rules[customRuleName] = ruleConfig;
1346
+ }
1347
+ continue;
1348
+ }
1349
+
1350
+ // For TypeScript rules (T series), use rule ID directly in custom plugin
1351
+ if (rule.id.startsWith('T')) {
1352
+ const customRuleName = `custom/${rule.id.toLowerCase()}`;
1353
+ const ruleConfig = this.mapSeverity(rule.severity || 'warning');
1354
+ config.rules[customRuleName] = ruleConfig;
1355
+ continue;
1356
+ }
1357
+
1358
+ // For other rules, check mapping file
598
1359
  const eslintRules = this.ruleMapping.get(rule.id);
599
1360
 
600
1361
  if (eslintRules && Array.isArray(eslintRules)) {
@@ -604,16 +1365,10 @@ class ESLintEngine extends AnalysisEngineInterface {
604
1365
  config.rules[eslintRule] = severity;
605
1366
  }
606
1367
  } else {
607
- // Check if it's a custom rule (C010, etc.)
1368
+ // Fallback - try as custom rule
608
1369
  const customRuleName = `custom/${rule.id.toLowerCase()}`;
609
1370
  const ruleConfig = this.mapSeverity(rule.severity || 'warning');
610
-
611
- // Add rule configuration for specific rules
612
- if (rule.id === 'C010') {
613
- config.rules[customRuleName] = [ruleConfig, { maxDepth: 3 }];
614
- } else {
615
- config.rules[customRuleName] = ruleConfig;
616
- }
1371
+ config.rules[customRuleName] = ruleConfig;
617
1372
  }
618
1373
  }
619
1374
 
@@ -732,6 +1487,22 @@ class ESLintEngine extends AnalysisEngineInterface {
732
1487
  * Following Rule C006: Verb-noun naming
733
1488
  */
734
1489
  async cleanup() {
1490
+ // Clean up temporary config files
1491
+ if (this.tempConfigPaths && this.tempConfigPaths.length > 0) {
1492
+ const fs = require('fs');
1493
+ for (const tempPath of this.tempConfigPaths) {
1494
+ try {
1495
+ if (fs.existsSync(tempPath)) {
1496
+ fs.unlinkSync(tempPath);
1497
+ console.log(`๐Ÿงน [ESLintEngine] Cleaned up temporary config: ${tempPath}`);
1498
+ }
1499
+ } catch (error) {
1500
+ console.warn(`โš ๏ธ [ESLintEngine] Failed to cleanup temp config ${tempPath}: ${error.message}`);
1501
+ }
1502
+ }
1503
+ this.tempConfigPaths = [];
1504
+ }
1505
+
735
1506
  this.eslint = null;
736
1507
  this.configFiles.clear();
737
1508
  this.ruleMapping.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sunlint",
3
- "version": "1.1.6",
3
+ "version": "1.1.7",
4
4
  "description": "โ˜€๏ธ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards",
5
5
  "main": "cli.js",
6
6
  "bin": {