UNPKG

lwc-linter

Version:

A comprehensive CLI tool for linting Lightning Web Components v8.0.0+ with modern LWC patterns, decorators, lifecycle hooks, and Salesforce platform integration

433 lines (428 loc) • 19.3 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const commander_1 = require("commander"); const chalk_1 = __importDefault(require("chalk")); const ora_1 = __importDefault(require("ora")); const open_1 = __importDefault(require("open")); const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const linter_1 = require("./core/linter"); const config_loader_1 = require("./core/config-loader"); const output_formatter_1 = require("./core/output-formatter"); const rule_manager_1 = require("./core/rule-manager"); const dev_server_1 = require("./core/dev-server"); const program = new commander_1.Command(); // Helper function to find and validate LWC path function resolveLWCPath(inputPath) { // If user provided a specific path, use it if (inputPath && inputPath !== '.') { if (fs.existsSync(inputPath)) { return path.resolve(inputPath); } else { console.error(chalk_1.default.red(`āŒ Path not found: ${inputPath}`)); process.exit(1); } } // Standard Salesforce DX LWC paths to check const standardPaths = [ 'force-app/main/default/lwc', 'force-app/main/default/lwc/', './force-app/main/default/lwc', './force-app/main/default/lwc/', 'src/lwc', 'src/lwc/', './src/lwc', './src/lwc/' ]; // Check for standard LWC paths for (const lwcPath of standardPaths) { const resolvedPath = path.resolve(lwcPath); if (fs.existsSync(resolvedPath)) { console.log(chalk_1.default.blue(`šŸ“ Auto-detected LWC path: ${lwcPath}`)); return resolvedPath; } } // If no standard paths found, use current directory const currentDir = path.resolve('.'); console.log(chalk_1.default.yellow(`āš ļø No standard LWC folder found. Using current directory: ${currentDir}`)); console.log(chalk_1.default.gray(`šŸ’” Expected paths: ${standardPaths.slice(0, 2).join(', ')}`)); return currentDir; } // Helper function to show LWC project info function showLWCProjectInfo(targetPath) { const lwcComponents = []; try { if (fs.existsSync(targetPath)) { const items = fs.readdirSync(targetPath, { withFileTypes: true }); for (const item of items) { if (item.isDirectory()) { const componentPath = path.join(targetPath, item.name); const hasJS = fs.existsSync(path.join(componentPath, `${item.name}.js`)); const hasHTML = fs.existsSync(path.join(componentPath, `${item.name}.html`)); const hasCSS = fs.existsSync(path.join(componentPath, `${item.name}.css`)); if (hasJS || hasHTML || hasCSS) { lwcComponents.push({ name: item.name, files: { js: hasJS, html: hasHTML, css: hasCSS } }); } } } } } catch (error) { // Ignore errors during component detection } if (lwcComponents.length > 0) { console.log(chalk_1.default.green(`\nšŸŽÆ Found ${lwcComponents.length} LWC component(s):`)); lwcComponents.slice(0, 5).forEach(comp => { const files = Object.entries(comp.files) .filter(([_, exists]) => exists) .map(([ext, _]) => ext.toUpperCase()) .join(', '); console.log(chalk_1.default.gray(` • ${comp.name} (${files})`)); }); if (lwcComponents.length > 5) { console.log(chalk_1.default.gray(` ... and ${lwcComponents.length - 5} more`)); } } } program .name('lwc-linter') .description('šŸ” A comprehensive CLI tool for linting Lightning Web Components with code quality, accessibility, performance, security, ESLint, and Prettier integration') .version('1.0.0'); // Main command (backwards compatibility) program .argument('[path]', 'Path to LWC directory (defaults to force-app/main/default/lwc/)', '.') .option('-c, --config <file>', 'Configuration file path') .option('-r, --report <format>', 'Output format (cli, json, html)', 'cli') .option('--fix', 'Enable auto-fixing of issues') .option('--rules [rules...]', 'Specific rules to run') .option('--disable [rules...]', 'Rules to disable') .option('--severity <level>', 'Minimum severity level (error, warn, info)', 'info') .option('--open', 'Open HTML report in browser (when using --report html)') .action(async (inputPath, options) => { const targetPath = resolveLWCPath(inputPath === '.' ? undefined : inputPath); console.log(chalk_1.default.blue.bold('\nšŸ” LWC Linter - Salesforce Lightning Web Components Analysis\n')); if (options.rules) { (0, rule_manager_1.listRules)(); return; } showLWCProjectInfo(targetPath); const spinner = (0, ora_1.default)('Loading configuration and initializing linter...').start(); try { const config = await (0, config_loader_1.loadConfig)(options.config); // Apply CLI options if (options.fix) config.fix = true; if (options.severity) config.severity = options.severity; if (options.rules && Array.isArray(options.rules)) { // If specific rules are requested, disable all others and enable only the requested ones const requestedRules = options.rules; Object.keys(config.rules).forEach(rule => { config.rules[rule] = requestedRules.includes(rule) ? 'warn' : 'off'; }); } if (options.disable) { options.disable.forEach((rule) => { if (config.rules) config.rules[rule] = 'off'; }); } spinner.text = `Analyzing LWC components in ${path.relative(process.cwd(), targetPath)}...`; const linter = new linter_1.LWCLinter(config); const results = await linter.lintDirectory(targetPath); spinner.succeed('Analysis completed!'); const formatter = new output_formatter_1.OutputFormatter(options.report); const output = await formatter.format(results); if (options.report === 'html') { const reportFile = 'lwc-lint-report.html'; await fs.writeFile(reportFile, output); console.log(chalk_1.default.green(`\nšŸ“Š HTML report saved: ${reportFile}`)); if (options.open) { await (0, open_1.default)(reportFile); console.log(chalk_1.default.blue('🌐 Report opened in browser')); } console.log(chalk_1.default.cyan(`\nšŸ’” For live dashboard with hot refresh, use: ${chalk_1.default.bold('npx lwc-linter serve')}`)); } else { console.log(output); } // Show auto-fix suggestion if (!options.fix && results.some(r => r.issues.some(i => i.fixable))) { console.log(chalk_1.default.yellow(`\nšŸ”§ Auto-fix available! Run: ${chalk_1.default.bold(`npx lwc-linter "${targetPath}" --fix`)}`)); } // Exit with appropriate code const hasErrors = results.some(r => r.issues.some(i => i.severity === 'error')); process.exit(hasErrors ? 1 : 0); } catch (error) { spinner.fail('Failed to analyze LWC components'); console.error(chalk_1.default.red(`Error: ${error instanceof Error ? error.message : error}`)); process.exit(1); } }); // Quick scan command (recommended) program .command('scan [path]') .description('šŸš€ Quick scan: lint LWC project, generate report, and open in browser') .option('-c, --config <file>', 'Configuration file path') .option('--no-open', 'Don\'t open browser automatically') .option('--fix', 'Show auto-fix suggestions') .action(async (inputPath = '.', options) => { const targetPath = resolveLWCPath(inputPath === '.' ? undefined : inputPath); console.log(chalk_1.default.blue.bold('\nšŸš€ LWC Quick Scan - Salesforce Lightning Web Components\n')); showLWCProjectInfo(targetPath); const spinner = (0, ora_1.default)('Scanning LWC components...').start(); try { const config = await (0, config_loader_1.loadConfig)(options.config); const linter = new linter_1.LWCLinter(config); const results = await linter.lintDirectory(targetPath); spinner.succeed('Scan completed!'); // Generate HTML report const formatter = new output_formatter_1.OutputFormatter('html'); const htmlOutput = await formatter.format(results); const reportFile = 'lwc-lint-report.html'; await fs.writeFile(reportFile, htmlOutput); // Show summary const totalIssues = results.reduce((sum, r) => sum + r.issues.length, 0); const totalErrors = results.reduce((sum, r) => sum + r.issues.filter(i => i.severity === 'error').length, 0); const totalWarnings = results.reduce((sum, r) => sum + r.issues.filter(i => i.severity === 'warn').length, 0); const fixableIssues = results.reduce((sum, r) => sum + r.issues.filter(i => i.fixable).length, 0); console.log(chalk_1.default.green(`\nšŸ“Š Scan Summary:`)); console.log(chalk_1.default.gray(` Files processed: ${results.length}`)); console.log(chalk_1.default.red(` Errors: ${totalErrors}`)); console.log(chalk_1.default.yellow(` Warnings: ${totalWarnings}`)); console.log(chalk_1.default.blue(` Info: ${totalIssues - totalErrors - totalWarnings}`)); if (fixableIssues > 0) { console.log(chalk_1.default.green(` Fixable: ${fixableIssues}`)); } console.log(chalk_1.default.green(`\nšŸ“„ Report saved: ${reportFile}`)); // Open in browser if (options.open) { await (0, open_1.default)(reportFile); console.log(chalk_1.default.blue('🌐 Report opened in browser')); } // Show next steps console.log(chalk_1.default.cyan(`\nšŸ’” Next steps:`)); console.log(chalk_1.default.gray(` • Live dashboard: ${chalk_1.default.bold('npx lwc-linter serve')}`)); if (fixableIssues > 0) { console.log(chalk_1.default.gray(` • Auto-fix issues: ${chalk_1.default.bold(`npx lwc-linter "${targetPath}" --fix`)}`)); } } catch (error) { spinner.fail('Scan failed'); console.error(chalk_1.default.red(`Error: ${error instanceof Error ? error.message : error}`)); process.exit(1); } }); // Live server command program .command('serve [path]') .description('šŸ”“ Start live development server with hot refresh for LWC components') .option('-c, --config <file>', 'Configuration file path') .option('-p, --port <number>', 'Server port (default: 3000)', '3000') .option('--no-open', 'Don\'t open browser automatically') .action(async (inputPath = '.', options) => { const targetPath = resolveLWCPath(inputPath === '.' ? undefined : inputPath); const port = parseInt(options.port); console.log(chalk_1.default.red.bold('\nšŸ”“ LWC Linter Live Server - Hot Refresh Dashboard\n')); showLWCProjectInfo(targetPath); try { const config = await (0, config_loader_1.loadConfig)(options.config); const devServer = new dev_server_1.DevServer(config, targetPath, port); console.log(chalk_1.default.blue('šŸ”“ Starting LWC Linter Live Server...')); await devServer.start(); // Open in browser if (options.open) { console.log(chalk_1.default.blue('🌐 Opening live dashboard in browser...')); await (0, open_1.default)(`http://localhost:${port}`); console.log(chalk_1.default.green('āœ… Dashboard opened successfully!')); } // Show features console.log(chalk_1.default.magenta('\nšŸŽÆ Live Dashboard Features:')); console.log(chalk_1.default.gray(' • Hot refresh: Instant updates when LWC files change')); console.log(chalk_1.default.gray(' • Interactive filtering and controls')); console.log(chalk_1.default.gray(' • All click events fully functional')); console.log(chalk_1.default.gray(' • Real-time file change detection')); console.log(chalk_1.default.gray(' • Export to Excel anytime')); console.log(chalk_1.default.green('\n✨ Your dashboard is now LIVE! Edit LWC files and watch them update instantly!')); // Graceful shutdown const cleanup = () => { devServer.stop(); process.exit(0); }; process.on('SIGINT', cleanup); process.on('SIGTERM', cleanup); } catch (error) { console.error(chalk_1.default.red(`Failed to start server: ${error instanceof Error ? error.message : error}`)); process.exit(1); } }); // List rules command program .command('rules') .description('šŸ“‹ List all available linting rules') .option('-c, --config <file>', 'Configuration file path') .action(async (options) => { try { (0, rule_manager_1.listRules)(); } catch (error) { console.error(chalk_1.default.red(`Error: ${error instanceof Error ? error.message : error}`)); process.exit(1); } }); // Init command to create config program .command('init') .description('šŸ› ļø Initialize LWC linter configuration') .action(async () => { const configPath = '.lwclintrc.json'; if (fs.existsSync(configPath)) { console.log(chalk_1.default.yellow(`āš ļø Configuration file already exists: ${configPath}`)); return; } const defaultConfig = { "rules": { "no-unused-vars": "warn", "no-console": "error", "no-debugger": "error", "prefer-const": "warn", "no-var": "error", "eqeqeq": "error", "curly": "warn", "no-trailing-spaces": "warn", "indent": "warn", "quotes": "warn", "semi": "warn", "aria-required": "error", "alt-text-required": "error", "focus-visible": "warn", "keyboard-navigation": "warn", "color-contrast": "info", "semantic-html": "warn", "avoid-dom-manipulation": "warn", "no-innerhtml": "error", "avoid-document-query": "warn", "limit-dom-nodes": "info", "optimize-list-rendering": "warn", "lazy-loading": "info", "no-inline-styles": "warn", "no-eval": "error", "validate-inputs": "error", "sanitize-html": "error", "secure-communication": "warn", "eslint-integration": "warn", "prettier-formatting": "warn", "import-organization": "info" }, "severity": "info", "fix": false, "exclude": [ "node_modules/**", "**/*.test.js", "**/*.spec.js", "**/test/**", "**/__tests__/**", "**/coverage/**", "lib/**", "dist/**" ], "include": [ "**/*.js", "**/*.html", "**/*.css" ], "accessibility": { "enabled": true, "level": "AA" }, "performance": { "enabled": true, "maxBundleSize": 500000, "maxDomNodes": 1000 }, "security": { "enabled": true, "allowInnerHTML": false }, "eslintIntegration": { "enabled": true, "configPath": ".eslintrc.js", "ignorePatterns": ["node_modules/**", "lib/**", "*.min.js"] }, "prettierIntegration": { "enabled": true, "configPath": ".prettierrc.js", "printWidth": 100, "tabWidth": 2, "useTabs": false } }; await fs.writeFile(configPath, JSON.stringify(defaultConfig, null, 2)); console.log(chalk_1.default.green(`āœ… Configuration file created: ${configPath}`)); console.log(chalk_1.default.blue('\nšŸ’” Next steps:')); console.log(chalk_1.default.gray(` • Edit ${configPath} to customize rules`)); console.log(chalk_1.default.gray(` • Run ${chalk_1.default.bold('npx lwc-linter scan')} to start linting`)); console.log(chalk_1.default.gray(` • Use ${chalk_1.default.bold('npx lwc-linter serve')} for live dashboard`)); }); // Help examples program.addHelpText('after', ` Examples: ${chalk_1.default.bold('Standard Salesforce DX project:')} $ npx lwc-linter scan # Auto-detects force-app/main/default/lwc/ $ npx lwc-linter serve # Live dashboard with hot refresh ${chalk_1.default.bold('Custom paths:')} $ npx lwc-linter scan src/lwc # Scan specific LWC directory $ npx lwc-linter "my-components/" --report html # Generate HTML report ${chalk_1.default.bold('Configuration:')} $ npx lwc-linter init # Create .lwclintrc.json $ npx lwc-linter rules # List all available rules ${chalk_1.default.bold('Auto-fixing:')} $ npx lwc-linter scan --fix # Show fixable issues $ npx lwc-linter force-app/main/default/lwc --fix # Auto-fix issues ${chalk_1.default.blue('šŸ’” Pro tip:')} Use ${chalk_1.default.bold('npx lwc-linter serve')} for the best experience with live hot refresh! `); program.parse(); //# sourceMappingURL=cli.js.map