renderpilot 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +121 -0
  2. package/package.json +65 -0
  3. package/src/cli/commands/scan.js +83 -0
  4. package/src/cli/index.js +49 -0
  5. package/src/cli/options.js +18 -0
  6. package/src/config/ConfigLoader.js +102 -0
  7. package/src/config/DefaultConfig.js +33 -0
  8. package/src/index.js +34 -0
  9. package/src/parser/ASTUtils.js +241 -0
  10. package/src/parser/BabelParser.js +66 -0
  11. package/src/parser/ComponentDetector.js +53 -0
  12. package/src/reporters/HtmlReporter.js +42 -0
  13. package/src/reporters/JsonReporter.js +52 -0
  14. package/src/reporters/TerminalReporter.js +91 -0
  15. package/src/reporters/templates/report.html.js +655 -0
  16. package/src/rules/base/AbstractRule.js +136 -0
  17. package/src/rules/engine/RuleEngine.js +67 -0
  18. package/src/rules/implementations/R001_MissingKeyProp.js +97 -0
  19. package/src/rules/implementations/R002_InfiniteUseEffect.js +86 -0
  20. package/src/rules/implementations/R003_IncorrectDepsArray.js +112 -0
  21. package/src/rules/implementations/R004_LargeComponent.js +47 -0
  22. package/src/rules/implementations/R005_InlineCallback.js +58 -0
  23. package/src/rules/implementations/R006_HeavyRenderCalc.js +86 -0
  24. package/src/rules/implementations/R007_MemoCandidate.js +87 -0
  25. package/src/rules/implementations/R008_NestedComponent.js +65 -0
  26. package/src/rules/implementations/R009_UnusedState.js +62 -0
  27. package/src/rules/index.js +37 -0
  28. package/src/rules/registry/RuleRegistry.js +67 -0
  29. package/src/scanner/FileScanner.js +42 -0
  30. package/src/scanner/ProjectScanner.js +116 -0
  31. package/src/score/CategoryScorer.js +53 -0
  32. package/src/score/ScoreCalculator.js +97 -0
  33. package/src/score/ScoreWeights.js +33 -0
  34. package/src/types/index.js +176 -0
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # RenderPilot ✈️
2
+
3
+ **The Intelligent React Performance Analysis CLI**
4
+
5
+ RenderPilot statically analyzes your React (**JS/TS/JSX/TSX**) codebase to detect performance anti-patterns, explain why they occur, estimate their impact, and suggest pragmatic fixes. It generates a comprehensive **Performance Score** and provides a developer-friendly interactive dashboard to track your fixes.
6
+
7
+ ---
8
+
9
+ ## 🌟 Key Features
10
+
11
+ - **Built for Modern Stacks:** Seamlessly supports TypeScript (`.ts`, `.tsx`) and modern JavaScript syntax out of the box using Babel. No complex configuration required.
12
+ - **Smart Component Detection:** Uses an advanced AST (Abstract Syntax Tree) engine to identify React Component boundaries. This means it safely ignores backend files (Express, NestJS), utility functions, and non-React files—preventing false positives.
13
+ - **Interactive Developer Dashboard:** Generates a stunning, Vercel-inspired HTML report with collapsible file accordions, line-number highlighting, and a **"Mark as Fixed"** workflow to easily track your debugging progress.
14
+ - **Pragmatic, Senior-Level Advice:** RenderPilot is designed to be a Performance Advisor, not a dogmatic linter.
15
+ - Differentiates between array mutations (`.sort()`) vs pure array methods (`.filter()`).
16
+ - Only recommends `React.memo` for components that actually receive props.
17
+ - **Performance Grading:** Get an intelligent overall grade (A-F) based on severity-weighted categories (Rendering, Hooks, Architecture, Maintainability).
18
+
19
+ ## 🚀 Installation
20
+
21
+ ```bash
22
+ npm install -g renderpilot
23
+ # or
24
+ npx renderpilot scan .
25
+ ```
26
+
27
+ ## 🛠️ Usage
28
+
29
+ Scan your current directory (Outputs to Terminal):
30
+ ```bash
31
+ renderpilot scan .
32
+ ```
33
+
34
+ **Recommended:** Generate the interactive HTML dashboard:
35
+ ```bash
36
+ renderpilot report .
37
+ # Equivalent to: renderpilot scan . --html
38
+ ```
39
+ *(This will automatically open `renderpilot-report.html` in your default browser).*
40
+
41
+ Generate a JSON payload for CI integration:
42
+ ```bash
43
+ renderpilot scan . --json
44
+ ```
45
+
46
+ Get just the score (useful for CI/CD pipeline gates):
47
+ ```bash
48
+ renderpilot scan . --score
49
+ ```
50
+
51
+ ## ⚙️ Configuration
52
+
53
+ RenderPilot works zero-config out of the box. To customize it, create a `renderpilot.config.js` file in your project root:
54
+
55
+ ```javascript
56
+ module.exports = {
57
+ // Directories to ignore during scan
58
+ ignoreDirs: ['node_modules', 'dist', 'build', '.next'],
59
+
60
+ // Maximum component lines before triggering a warning
61
+ maxComponentLines: 300,
62
+
63
+ // Override severity for specific rules
64
+ severityOverrides: {
65
+ 'R001': 'critical', // Missing key prop
66
+ },
67
+
68
+ // Explicitly disable certain rules
69
+ disabledRules: ['R004', 'R007'],
70
+
71
+ // Output directory for HTML and JSON reports
72
+ outputDir: '.renderpilot'
73
+ };
74
+ ```
75
+
76
+ ## 📏 Core Rules
77
+
78
+ | ID | Name | Category | Severity |
79
+ |----|------|----------|----------|
80
+ | **R001** | Missing Key Prop | Rendering | Warning |
81
+ | **R002** | Infinite useEffect Loop | Hooks | Critical |
82
+ | **R003** | Incorrect Dependency Array | Hooks | Warning |
83
+ | **R004** | Large Component | Architecture | Info |
84
+ | **R005** | Inline Callback Detection | Rendering | Info |
85
+ | **R006** | Heavy Calculations in Render | Rendering | Warning |
86
+ | **R007** | React.memo Candidate | Rendering | Info |
87
+ | **R008** | Nested Component Declarations | Architecture | Critical |
88
+ | **R009** | Unused React State | Maintainability| Warning |
89
+
90
+ ## 💯 How the Scoring Works
91
+
92
+ Your project starts with 100 points. Points are deducted based on rule violations, severity, and category weight.
93
+ - **Critical Issues (-15 points)** (Multiplied by category weight)
94
+ - **Warnings (-5 points)**
95
+ - **Info (-1 point)**
96
+
97
+ Categories are weighted by performance impact:
98
+ - **Rendering** (1.5x)
99
+ - **Hooks** (1.3x)
100
+ - **Architecture** (1.0x)
101
+ - **Maintainability** (0.7x)
102
+
103
+ ## 💻 Programmatic API
104
+
105
+ You can use RenderPilot as a Node.js library to integrate it into your own tools:
106
+
107
+ ```javascript
108
+ const { ProjectScanner, ScoreCalculator, loadConfig } = require('renderpilot');
109
+
110
+ async function run() {
111
+ const config = loadConfig('./src');
112
+ const scanResult = await ProjectScanner.scan('./src', config);
113
+ const score = ScoreCalculator.compute(scanResult.allViolations);
114
+
115
+ console.log('Score:', score.overall);
116
+ }
117
+ ```
118
+
119
+ ## 📄 License
120
+
121
+ MIT © RenderPilot Contributors
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "renderpilot",
3
+ "version": "1.0.0",
4
+ "description": "The Intelligent React (JS/TSX) Performance Analysis CLI — detects anti-patterns, generates interactive HTML reports, and provides pragmatic fixes.",
5
+ "keywords": [
6
+ "react",
7
+ "performance",
8
+ "cli",
9
+ "static-analysis",
10
+ "ast",
11
+ "jsx",
12
+ "tsx",
13
+ "react-performance",
14
+ "code-analysis",
15
+ "developer-tools"
16
+ ],
17
+ "author": "RenderPilot Contributors",
18
+ "license": "MIT",
19
+ "homepage": "https://github.com/ADITYA-user18/renderpilot#readme",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/ADITYA-user18/renderpilot.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/ADITYA-user18/renderpilot/issues"
26
+ },
27
+ "main": "./src/index.js",
28
+ "bin": {
29
+ "renderpilot": "./src/cli/index.js"
30
+ },
31
+ "files": [
32
+ "src",
33
+ "README.md",
34
+ "CHANGELOG.md"
35
+ ],
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ },
39
+ "scripts": {
40
+ "start": "node src/cli/index.js",
41
+ "dev": "node src/cli/index.js",
42
+ "test": "jest",
43
+ "test:watch": "jest --watch",
44
+ "test:coverage": "jest --coverage",
45
+ "lint": "eslint src",
46
+ "lint:fix": "eslint src --fix",
47
+ "format": "prettier --write \"src/**/*.js\""
48
+ },
49
+ "dependencies": {
50
+ "@babel/parser": "^7.24.0",
51
+ "@babel/traverse": "^7.24.0",
52
+ "@babel/types": "^7.24.0",
53
+ "chalk": "^4.1.2",
54
+ "commander": "^12.1.0",
55
+ "fast-glob": "^3.3.2",
56
+ "open": "^8.4.2",
57
+ "ora": "^5.4.1",
58
+ "zod": "^3.23.8"
59
+ },
60
+ "devDependencies": {
61
+ "jest": "^29.7.0",
62
+ "prettier": "^3.3.2"
63
+ },
64
+ "type": "commonjs"
65
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * @file cli/commands/scan.js
3
+ * @description
4
+ * The main 'scan' command.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const path = require('path');
10
+ const ora = require('ora');
11
+ const chalk = require('chalk');
12
+ const open = require('open');
13
+ const { loadConfig } = require('../../config/ConfigLoader');
14
+ const { ProjectScanner } = require('../../scanner/ProjectScanner');
15
+ const { ScoreCalculator } = require('../../score/ScoreCalculator');
16
+ const { TerminalReporter } = require('../../reporters/TerminalReporter');
17
+ const { JsonReporter } = require('../../reporters/JsonReporter');
18
+ const { HtmlReporter } = require('../../reporters/HtmlReporter');
19
+
20
+ /**
21
+ * Handle the 'scan' command.
22
+ * @param {string} targetDir
23
+ * @param {object} cmdOptions
24
+ */
25
+ async function handleScan(targetDir = '.', cmdOptions) {
26
+ const rootDir = path.resolve(process.cwd(), targetDir);
27
+
28
+ // 1. Load config
29
+ const config = loadConfig(rootDir); // In the future, cmdOptions.config can override
30
+
31
+ // 2. Initialize Spinner
32
+ const spinner = ora(`Discovering React files in ${chalk.bold(rootDir)}...`).start();
33
+
34
+ try {
35
+ // 3. Run Scan
36
+ const scanResult = await ProjectScanner.scan(rootDir, config);
37
+
38
+ spinner.succeed(`Scanned ${scanResult.totalFiles} files in ${scanResult.durationMs}ms`);
39
+
40
+ // 4. Calculate Score
41
+ const score = ScoreCalculator.compute(scanResult.allViolations);
42
+
43
+ const reportData = {
44
+ version: require('../../../package.json').version,
45
+ scan: scanResult,
46
+ score,
47
+ config,
48
+ };
49
+
50
+ // 5. Generate Outputs
51
+ if (cmdOptions.score) {
52
+ // Just print the score
53
+ console.log(`\n🏆 Performance Score: ${chalk.bold.green(score.overall + '/100')} (${score.grade})`);
54
+ return;
55
+ }
56
+
57
+ TerminalReporter.generate(reportData);
58
+
59
+ if (cmdOptions.json) {
60
+ const jsonPath = JsonReporter.generate(reportData);
61
+ console.log(`💾 JSON report saved to: ${chalk.blue(jsonPath)}`);
62
+ }
63
+
64
+ if (cmdOptions.html) {
65
+ const htmlPath = HtmlReporter.generate(reportData);
66
+ console.log(`🌐 HTML report saved to: ${chalk.blue(htmlPath)}`);
67
+
68
+ // Auto-open if possible
69
+ try {
70
+ await open(htmlPath);
71
+ } catch (err) {
72
+ console.log(chalk.dim(`(Could not auto-open browser: ${err.message})`));
73
+ }
74
+ }
75
+
76
+ } catch (error) {
77
+ spinner.fail('Scan failed due to an internal error.');
78
+ console.error(chalk.red('\nError Details:'), error);
79
+ process.exit(1);
80
+ }
81
+ }
82
+
83
+ module.exports = { handleScan };
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @file cli/index.js
5
+ * @description
6
+ * Main CLI entry point. Wires up Commander and parses arguments.
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const { Command } = require('commander');
12
+ const { version, description } = require('../../package.json');
13
+ const { handleScan } = require('./commands/scan');
14
+ const { options } = require('./options');
15
+
16
+ // Ensure rules are registered before anything runs
17
+ require('../rules/index');
18
+
19
+ const program = new Command();
20
+
21
+ program
22
+ .name('renderpilot')
23
+ .description(description)
24
+ .version(version);
25
+
26
+ program
27
+ .command('scan [directory]')
28
+ .description('Scan a React project for performance issues')
29
+ .addOption(options.json)
30
+ .addOption(options.html)
31
+ .addOption(options.score)
32
+ .addOption(options.config)
33
+ .action((directory, cmdOptions) => {
34
+ handleScan(directory, cmdOptions);
35
+ });
36
+
37
+ // renderpilot report is just an alias for scan --html for now
38
+ program
39
+ .command('report [directory]')
40
+ .description('Scan a project and immediately open the HTML report')
41
+ .action((directory) => {
42
+ handleScan(directory, { html: true });
43
+ });
44
+
45
+ program.parse(process.argv);
46
+
47
+ if (!process.argv.slice(2).length) {
48
+ program.outputHelp();
49
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @file cli/options.js
3
+ * @description
4
+ * Reusable Commander.js options shared across multiple commands.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { Option } = require('commander');
10
+
11
+ const options = {
12
+ json: new Option('--json', 'Output results as a JSON file').default(false),
13
+ html: new Option('--html', 'Output results as an interactive HTML report').default(false),
14
+ score: new Option('--score', 'Only print the overall performance score').default(false),
15
+ config: new Option('-c, --config <path>', 'Path to custom config file'),
16
+ };
17
+
18
+ module.exports = { options };
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @file config/ConfigLoader.js
3
+ * @description
4
+ * Loads and merges user configuration from renderpilot.config.js
5
+ * with built-in defaults. Performs validation and normalization.
6
+ *
7
+ * Resolution order:
8
+ * 1. Look for renderpilot.config.js in the scan root
9
+ * 2. Look for renderpilot.config.js in process.cwd()
10
+ * 3. Fall back to DEFAULT_CONFIG
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const path = require('path');
16
+ const fs = require('fs');
17
+ const { DEFAULT_CONFIG } = require('./DefaultConfig');
18
+
19
+ /**
20
+ * Attempts to load a renderpilot.config.js file from the given directory.
21
+ * @param {string} dir - Directory to search in
22
+ * @returns {import('../types').UserConfig|null} User config or null if not found
23
+ */
24
+ function loadConfigFile(dir) {
25
+ const configPath = path.join(dir, 'renderpilot.config.js');
26
+ if (!fs.existsSync(configPath)) return null;
27
+
28
+ try {
29
+ // Clear require cache to support hot-reloading in watch mode
30
+ delete require.cache[require.resolve(configPath)];
31
+ return require(configPath);
32
+ } catch (err) {
33
+ console.warn(
34
+ `[RenderPilot] Warning: Failed to load config at ${configPath}: ${err.message}`
35
+ );
36
+ return null;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Merges user config with defaults and normalizes into a ResolvedConfig.
42
+ * @param {import('../types').UserConfig} userConfig - User-provided config
43
+ * @returns {import('../types').ResolvedConfig} Resolved config
44
+ */
45
+ function mergeConfig(userConfig) {
46
+ const merged = { ...DEFAULT_CONFIG };
47
+
48
+ if (Array.isArray(userConfig.ignoreDirs)) {
49
+ // Merge user ignore dirs with defaults (union)
50
+ merged.ignoreDirs = [
51
+ ...new Set([...DEFAULT_CONFIG.ignoreDirs, ...userConfig.ignoreDirs]),
52
+ ];
53
+ }
54
+
55
+ if (typeof userConfig.maxComponentLines === 'number' && userConfig.maxComponentLines > 0) {
56
+ merged.maxComponentLines = userConfig.maxComponentLines;
57
+ }
58
+
59
+ if (userConfig.severityOverrides && typeof userConfig.severityOverrides === 'object') {
60
+ merged.severityOverrides = { ...userConfig.severityOverrides };
61
+ }
62
+
63
+ if (Array.isArray(userConfig.enabledRules) && userConfig.enabledRules.length > 0) {
64
+ merged.enabledRules = new Set(userConfig.enabledRules);
65
+ }
66
+
67
+ if (Array.isArray(userConfig.disabledRules) && userConfig.disabledRules.length > 0) {
68
+ merged.disabledRules = new Set(userConfig.disabledRules);
69
+ }
70
+
71
+ if (typeof userConfig.outputDir === 'string' && userConfig.outputDir.trim()) {
72
+ merged.outputDir = userConfig.outputDir.trim();
73
+ }
74
+
75
+ merged.hasCustomConfig = true;
76
+ return merged;
77
+ }
78
+
79
+ /**
80
+ * Loads and resolves configuration for a scan.
81
+ * Searches scanRoot first, then cwd, then uses defaults.
82
+ *
83
+ * @param {string} scanRoot - The root directory being scanned
84
+ * @returns {import('../types').ResolvedConfig} Fully resolved config
85
+ */
86
+ function loadConfig(scanRoot) {
87
+ // Try scan root first
88
+ let userConfig = loadConfigFile(scanRoot);
89
+
90
+ // Fall back to cwd if not in scan root
91
+ if (!userConfig && scanRoot !== process.cwd()) {
92
+ userConfig = loadConfigFile(process.cwd());
93
+ }
94
+
95
+ if (!userConfig) {
96
+ return { ...DEFAULT_CONFIG };
97
+ }
98
+
99
+ return mergeConfig(userConfig);
100
+ }
101
+
102
+ module.exports = { loadConfig };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @file config/DefaultConfig.js
3
+ * @description Built-in default configuration for RenderPilot.
4
+ * All defaults live here — never scattered across modules.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ /** @type {import('../types').ResolvedConfig} */
10
+ const DEFAULT_CONFIG = {
11
+ ignoreDirs: [
12
+ 'node_modules',
13
+ 'dist',
14
+ 'build',
15
+ 'coverage',
16
+ '.git',
17
+ '.next',
18
+ '.nuxt',
19
+ 'out',
20
+ '.cache',
21
+ 'public',
22
+ 'static',
23
+ 'assets',
24
+ ],
25
+ maxComponentLines: 300,
26
+ severityOverrides: {},
27
+ enabledRules: null, // null = all rules enabled
28
+ disabledRules: new Set(),
29
+ outputDir: '.renderpilot',
30
+ hasCustomConfig: false,
31
+ };
32
+
33
+ module.exports = { DEFAULT_CONFIG };
package/src/index.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @file index.js
3
+ * @description
4
+ * Main package export. Allows RenderPilot to be consumed programmatically
5
+ * by other Node.js scripts instead of just via the CLI.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ // Ensure rules are registered
11
+ require('./rules/index');
12
+
13
+ const { ProjectScanner } = require('./scanner/ProjectScanner');
14
+ const { ScoreCalculator } = require('./score/ScoreCalculator');
15
+ const { loadConfig } = require('./config/ConfigLoader');
16
+ const { ruleRegistry } = require('./rules/registry/RuleRegistry');
17
+
18
+ module.exports = {
19
+ // Core APIs
20
+ ProjectScanner,
21
+ ScoreCalculator,
22
+ loadConfig,
23
+
24
+ // Rule Registry access for custom rules
25
+ ruleRegistry,
26
+
27
+ // Re-export base rule for custom rule creation
28
+ AbstractRule: require('./rules/base/AbstractRule').AbstractRule,
29
+
30
+ // Reporters
31
+ TerminalReporter: require('./reporters/TerminalReporter').TerminalReporter,
32
+ JsonReporter: require('./reporters/JsonReporter').JsonReporter,
33
+ HtmlReporter: require('./reporters/HtmlReporter').HtmlReporter,
34
+ };