UNPKG

dev-with-debug

Version:

Auto-start Chrome-based dev server with debugging enabled, streaming LLM-friendly error output

717 lines (587 loc) โ€ข 23.8 kB
const fs = require('fs'); const path = require('path'); const spawn = require('cross-spawn'); const chromeLauncher = require('chrome-launcher'); const CDP = require('chrome-remote-interface'); class DevWithDebug { constructor() { this.chrome = null; this.cdp = null; this.devServer = null; } // Find available Chromium-based browser findChromiumBrowser() { const fs = require('fs'); const os = require('os'); let possiblePaths = []; // Platform-specific browser paths switch (os.platform()) { case 'darwin': // macOS possiblePaths = [ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser', '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', '/Applications/Opera.app/Contents/MacOS/Opera', '/Applications/Chromium.app/Contents/MacOS/Chromium' ]; break; case 'win32': // Windows const programFiles = process.env['ProgramFiles'] || 'C:\\Program Files'; const programFilesX86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)'; const localAppData = process.env['LOCALAPPDATA'] || `${os.homedir()}\\AppData\\Local`; possiblePaths = [ `${programFiles}\\Google\\Chrome\\Application\\chrome.exe`, `${programFilesX86}\\Google\\Chrome\\Application\\chrome.exe`, `${localAppData}\\Google\\Chrome\\Application\\chrome.exe`, `${programFiles}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`, `${programFilesX86}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`, `${localAppData}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`, `${programFiles}\\Microsoft\\Edge\\Application\\msedge.exe`, `${programFilesX86}\\Microsoft\\Edge\\Application\\msedge.exe`, `${programFiles}\\Opera\\opera.exe`, `${programFilesX86}\\Opera\\opera.exe`, `${localAppData}\\Programs\\Opera\\opera.exe` ]; break; case 'linux': // Linux possiblePaths = [ '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/usr/bin/chromium-browser', '/usr/bin/brave-browser', '/usr/bin/microsoft-edge', '/usr/bin/opera', '/snap/bin/chromium', '/snap/bin/brave', '/opt/google/chrome/chrome', '/opt/brave.com/brave/brave-browser' ]; break; default: console.log(`โš ๏ธ Unsupported platform: ${os.platform()}`); return undefined; } for (const browserPath of possiblePaths) { if (fs.existsSync(browserPath)) { console.log(`๐ŸŒ Using browser: ${browserPath}`); return browserPath; } } // Let chrome-launcher auto-detect if none found return undefined; } // Debug what's actually listening on the system async debugSystemPorts() { const { spawn } = require('child_process'); console.log(`๐Ÿ” Debugging system ports...`); try { // Check what's listening on localhost const lsofResult = await new Promise((resolve, reject) => { const proc = spawn('lsof', ['-i', ':9222'], { stdio: 'pipe' }); let output = ''; proc.stdout.on('data', data => output += data.toString()); proc.on('close', code => resolve(output)); proc.on('error', reject); }); console.log(`๐Ÿ“Š Processes on port 9222:\n${lsofResult}`); // Check chrome processes const psResult = await new Promise((resolve, reject) => { const proc = spawn('ps', ['aux'], { stdio: 'pipe' }); let output = ''; proc.stdout.on('data', data => output += data.toString()); proc.on('close', code => { const chromeLines = output.split('\n').filter(line => line.includes('Brave') || line.includes('Chrome') || line.includes('chromium') ); resolve(chromeLines.join('\n')); }); proc.on('error', reject); }); console.log(`๐Ÿ–ฅ๏ธ Browser processes:\n${psResult}`); } catch (err) { console.log(`โš ๏ธ Could not debug system ports: ${err.message}`); } } // Wait for debugging port to be ready async waitForDebugPort(port, maxRetries = 15) { const http = require('http'); console.log(`๐Ÿ” Checking if debug port ${port} is ready...`); // First, debug what's actually running await this.debugSystemPorts(); for (let i = 0; i < maxRetries; i++) { try { console.log(`๐ŸŒ Trying to connect to http://localhost:${port}/json/version`); const response = await new Promise((resolve, reject) => { const req = http.get(`http://localhost:${port}/json/version`, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { resolve({ status: res.statusCode, data, headers: res.headers }); }); }); req.on('error', (err) => { reject(err); }); req.setTimeout(3000, () => { req.destroy(); reject(new Error('Timeout')); }); }); console.log(`๐Ÿ“‹ Debug endpoint response (${response.status}): ${response.data}`); if (response.status === 200) { console.log(`โœ… Debug port ${port} is ready`); return; } else { throw new Error(`HTTP ${response.status}`); } } catch (err) { console.log(`โš ๏ธ Debug port ${port} attempt ${i + 1}/${maxRetries} failed: ${err.message}`); if (i === maxRetries - 1) { console.error(`โŒ Debug port ${port} failed after ${maxRetries} attempts`); console.error(` This suggests the browser is not running with debugging enabled`); console.error(` Let's try to understand why...`); // Final system debug await this.debugSystemPorts(); throw new Error(`Debug port ${port} not ready: ${err.message}`); } await new Promise(resolve => setTimeout(resolve, 2000)); } } } // Detect the dev server command from package.json detectDevCommand() { const packageJsonPath = path.join(process.cwd(), 'package.json'); if (!fs.existsSync(packageJsonPath)) { throw new Error('No package.json found in current directory'); } const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); const scripts = packageJson.scripts || {}; // Common dev server script names const devCommands = ['dev', 'start', 'serve', 'develop']; for (const cmd of devCommands) { if (scripts[cmd]) { return { command: 'npm', args: ['run', cmd] }; } } // Check for specific framework patterns if (scripts.vite) return { command: 'npm', args: ['run', 'vite'] }; if (scripts['webpack-dev-server']) return { command: 'npm', args: ['run', 'webpack-dev-server'] }; throw new Error('No dev server command found. Please ensure you have a "dev", "start", or "serve" script in package.json'); } // Parse actual DevTools port from error logs parseActualDevToolsPort(errorMessage) { const match = errorMessage.match(/DevTools listening on ws:\/\/127\.0\.0\.1:(\d+)/); return match ? parseInt(match[1]) : null; } // Discover actual debugging port async discoverActualPort() { const http = require('http'); // Try to find the actual port by checking what chrome-launcher returned console.log(`๐Ÿ” Chrome launcher returned port: ${this.chrome.port}`); console.log(`๐Ÿ” Chrome launcher object keys: ${Object.keys(this.chrome)}`); console.log(`๐Ÿ” Chrome launcher full object:`, this.chrome); // Check if there are any other port-related properties const portProperties = Object.keys(this.chrome).filter(key => key.toLowerCase().includes('port') || key.toLowerCase().includes('debug') ); console.log(`๐Ÿ” Port-related properties: ${portProperties}`); // Also try to query the browser directly for debugging endpoints try { const commonPorts = [this.chrome.port, 9222, 9223, 9224, 9225]; for (const port of commonPorts) { try { console.log(`๐ŸŒ Testing port ${port} for debugging endpoint...`); const response = await new Promise((resolve, reject) => { const req = http.get(`http://localhost:${port}/json/version`, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => resolve({ port, status: res.statusCode, data })); }); req.on('error', reject); req.setTimeout(1000, () => { req.destroy(); reject(new Error('Timeout')); }); }); if (response.status === 200) { console.log(`โœ… Found working debugging port: ${response.port}`); console.log(`๐Ÿ“‹ Version data: ${response.data}`); return response.port; } } catch (err) { console.log(`โŒ Port ${port} failed: ${err.message}`); } } throw new Error('No working debugging port found'); } catch (err) { throw new Error(`Could not discover actual debugging port: ${err.message}`); } } // Read DevToolsActivePort file to get actual debugging port async readDevToolsActivePort(userDataDir) { const fs = require('fs'); const path = require('path'); const portFilePath = path.join(userDataDir, 'DevToolsActivePort'); try { if (fs.existsSync(portFilePath)) { const content = fs.readFileSync(portFilePath, 'utf8'); const port = parseInt(content.split('\n')[0]); console.log(`๐Ÿ“‹ DevToolsActivePort file shows port: ${port}`); return port; } } catch (err) { console.log(`โš ๏ธ Could not read DevToolsActivePort: ${err.message}`); } return null; } // Launch Chrome with debugging enabled async launchChrome(devServerUrl = 'http://localhost:3000') { console.log(`๐Ÿš€ Launching browser with URL: ${devServerUrl}`); const browserPath = this.findChromiumBrowser(); const os = require('os'); const path = require('path'); const fs = require('fs'); // Create a cross-platform temp directory const userDataDir = path.join(os.tmpdir(), 'dev-with-debug-chrome'); // Ensure the user data directory exists if (!fs.existsSync(userDataDir)) { fs.mkdirSync(userDataDir, { recursive: true }); console.log(`๐Ÿ“ Created user data directory: ${userDataDir}`); } console.log(`๐Ÿ”ง Chrome launcher config:`); console.log(` Browser path: ${browserPath}`); console.log(` Starting URL: ${devServerUrl}`); console.log(` User data dir: ${userDataDir}`); // Use FIXED debugging port as recommended in the documentation const fixedDebugPort = 9222; this.chrome = await chromeLauncher.launch({ chromePath: browserPath, startingUrl: devServerUrl, userDataDir: userDataDir, port: fixedDebugPort, chromeFlags: [ '--disable-features=VizDisplayCompositor', '--no-first-run', '--auto-open-devtools-for-tabs' ] }); console.log(`๐Ÿ”— Browser launched successfully!`); console.log(` Process ID: ${this.chrome.pid}`); console.log(` Chrome-launcher returned port: ${this.chrome.port}`); console.log(` Expected debugging port: ${fixedDebugPort}`); // Force the port to our fixed port regardless of what chrome-launcher says this.chrome.port = fixedDebugPort; console.log(`๐Ÿ”Œ Forcing debugging port to: ${fixedDebugPort}`); // Wait a moment for browser to fully start await new Promise(resolve => setTimeout(resolve, 2000)); // Try to read the actual port from DevToolsActivePort file for verification const activePort = await this.readDevToolsActivePort(userDataDir); if (activePort && activePort !== fixedDebugPort) { console.log(`โš ๏ธ DevToolsActivePort shows different port: ${activePort}`); console.log(` But we'll try our fixed port first: ${fixedDebugPort}`); } // Verify the port is accessible await this.waitForDebugPort(fixedDebugPort, 5); // Shorter retry since we know the port // Connect to Chrome DevTools Protocol await this.connectToCDP(); console.log(`โœ… Connected to Chrome DevTools Protocol`); } // Manual port discovery without chrome-launcher async discoverActualPortManual() { const http = require('http'); const commonPorts = [9222, 9223, 9224, 9225, 9226, 9227, 9228, 9229]; console.log(`๐Ÿ” Scanning common debugging ports...`); for (const port of commonPorts) { try { const response = await new Promise((resolve, reject) => { const req = http.get(`http://localhost:${port}/json/version`, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => resolve({ port, status: res.statusCode, data })); }); req.on('error', reject); req.setTimeout(500, () => { req.destroy(); reject(new Error('Timeout')); }); }); if (response.status === 200) { console.log(`โœ… Found active debugging port: ${response.port}`); return response.port; } } catch (err) { // Port not available, continue } } return null; } // Debug manual browser launch async debugManualLaunch(browserPath, url) { const { spawn } = require('child_process'); try { console.log(`๐Ÿงช Testing manual launch: ${browserPath}`); const testProcess = spawn(browserPath, [ '--remote-debugging-port=9222', '--no-first-run', url ], { stdio: 'pipe', detached: true }); let output = ''; testProcess.stdout.on('data', data => output += data.toString()); testProcess.stderr.on('data', data => output += data.toString()); setTimeout(() => { console.log(`๐Ÿ“‹ Manual launch output: ${output.slice(0, 500)}`); testProcess.kill(); }, 3000); } catch (err) { console.log(`โš ๏ธ Manual launch test failed: ${err.message}`); } } // Connect to CDP with retry logic async connectToCDP(maxRetries = 5) { for (let i = 0; i < maxRetries; i++) { try { console.log(`๐Ÿ”Œ Attempting CDP connection... (attempt ${i + 1}/${maxRetries})`); this.cdp = await CDP({ port: this.chrome.port }); const { Page, Runtime, Console } = this.cdp; // Enable domains await Page.enable(); await Runtime.enable(); await Console.enable(); // Set up error listeners this.setupErrorListeners(); return; // Success, exit retry loop } catch (err) { if (i === maxRetries - 1) { throw new Error(`Failed to connect to CDP after ${maxRetries} attempts: ${err.message}`); } console.log(`โš ๏ธ CDP connection failed (attempt ${i + 1}): ${err.message}`); await new Promise(resolve => setTimeout(resolve, 2000)); } } } // Set up Chrome DevTools Protocol listeners setupErrorListeners() { const { Runtime, Console } = this.cdp; // Runtime errors Runtime.exceptionThrown((params) => { this.formatAndLogError('RUNTIME_ERROR', params.exceptionDetails); }); // Console errors Console.messageAdded((params) => { if (params.message.level === 'error') { this.formatAndLogError('CONSOLE_ERROR', params.message); } }); // Unhandled promise rejections Runtime.consoleAPICalled((params) => { if (params.type === 'error') { this.formatAndLogError('CONSOLE_API_ERROR', params); } }); } // Format errors for LLM consumption formatAndLogError(type, details) { const timestamp = new Date().toISOString(); let formatted = { timestamp, type, message: '', stack: '', location: '' }; try { switch (type) { case 'RUNTIME_ERROR': formatted.message = details.exception?.description || details.text || 'Unknown runtime error'; formatted.stack = details.exception?.stack || ''; formatted.location = `${details.url}:${details.lineNumber}:${details.columnNumber}`; break; case 'CONSOLE_ERROR': formatted.message = details.text || 'Console error'; formatted.location = `${details.url}:${details.line}:${details.column}`; break; case 'CONSOLE_API_ERROR': formatted.message = details.args?.map(arg => arg.value).join(' ') || 'Console API error'; formatted.location = details.stackTrace?.callFrames?.[0] ? `${details.stackTrace.callFrames[0].url}:${details.stackTrace.callFrames[0].lineNumber}` : ''; break; } // Output in LLM-friendly format console.log('\n๐Ÿšจ DEBUG ERROR:'); console.log(`Type: ${formatted.type}`); console.log(`Time: ${formatted.timestamp}`); console.log(`Message: ${formatted.message}`); if (formatted.location) console.log(`Location: ${formatted.location}`); if (formatted.stack) console.log(`Stack: ${formatted.stack.split('\n')[0]}`); // First line only for brevity console.log('โ”€'.repeat(50)); } catch (err) { console.error('Error formatting debug output:', err.message); } } // Parse dev server URL from output parseDevServerUrl(output) { // Common patterns for dev server URLs const patterns = [ /Local:\s+https?:\/\/[^\s]+/gi, /http:\/\/localhost:\d+/gi, /https:\/\/localhost:\d+/gi, /http:\/\/127\.0\.0\.1:\d+/gi, /https:\/\/127\.0\.0\.1:\d+/gi, /Server running at\s+https?:\/\/[^\s]+/gi, /Available on:\s*\n.*?https?:\/\/[^\s]+/gi, /Dev server running at:\s*https?:\/\/[^\s]+/gi ]; for (const pattern of patterns) { const matches = output.match(pattern); if (matches) { // Extract just the URL part const urlMatch = matches[0].match(/(https?:\/\/[^\s]+)/); if (urlMatch) { return urlMatch[1].replace(/\/$/, ''); // Remove trailing slash } } } return null; } // Wait for dev server to be ready async waitForDevServer(url, maxRetries = 20) { const http = require('http'); console.log(`๐Ÿ” Waiting for dev server at ${url}...`); for (let i = 0; i < maxRetries; i++) { try { await new Promise((resolve, reject) => { const req = http.get(url, (res) => { resolve(); }); req.on('error', reject); req.setTimeout(1000, () => { req.destroy(); reject(new Error('Timeout')); }); }); console.log(`โœ… Dev server is ready at ${url}`); return url; } catch (err) { if (i === maxRetries - 1) { console.error(`โŒ Dev server not ready after ${maxRetries} attempts`); throw new Error(`Dev server not ready at ${url}`); } console.log(`โณ Waiting for dev server... (attempt ${i + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, 2000)); } } } // Start the dev server async startDevServer() { const devCommand = this.detectDevCommand(); console.log(`๐Ÿš€ Starting dev server: ${devCommand.command} ${devCommand.args.join(' ')}`); let devServerOutput = ''; let detectedUrl = null; this.devServer = spawn(devCommand.command, devCommand.args, { stdio: ['inherit', 'pipe', 'pipe'], env: { ...process.env, NODE_ENV: 'development' } }); // Capture output to detect URL this.devServer.stdout.on('data', (data) => { const chunk = data.toString(); process.stdout.write(chunk); // Still show output to user devServerOutput += chunk; // Try to detect URL from new output if (!detectedUrl) { detectedUrl = this.parseDevServerUrl(devServerOutput); if (detectedUrl) { console.log(`๐ŸŒ Detected dev server URL: ${detectedUrl}`); } } }); this.devServer.stderr.on('data', (data) => { const chunk = data.toString(); process.stderr.write(chunk); // Still show errors to user devServerOutput += chunk; // Some dev servers output URL to stderr if (!detectedUrl) { detectedUrl = this.parseDevServerUrl(devServerOutput); if (detectedUrl) { console.log(`๐ŸŒ Detected dev server URL: ${detectedUrl}`); } } }); this.devServer.on('error', (err) => { console.error('โŒ Dev server error:', err.message); }); // Wait for URL detection or fallback to common ports let attempts = 0; const maxAttempts = 30; // 30 seconds while (!detectedUrl && attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 1000)); attempts++; if (attempts % 5 === 0) { console.log(`โณ Still waiting for dev server URL... (${attempts}s)`); } } // Fallback URLs if detection fails const fallbackUrls = [ 'http://localhost:3000', 'http://localhost:8080', 'http://localhost:4200', 'http://localhost:5000', 'http://localhost:8000', 'http://localhost:3001' ]; if (!detectedUrl) { console.log(`โš ๏ธ Could not detect dev server URL from output. Trying common ports...`); for (const url of fallbackUrls) { try { await this.waitForDevServer(url, 3); // Quick check detectedUrl = url; break; } catch (err) { // Continue to next URL } } } if (!detectedUrl) { throw new Error('Could not detect dev server URL. Please ensure your dev server is running and outputs its URL.'); } // Final verification that URL is accessible await this.waitForDevServer(detectedUrl); return detectedUrl; } // Main execution function async start() { try { console.log('๐Ÿ”ง Starting dev-with-debug...'); const devServerUrl = await this.startDevServer(); await this.launchChrome(devServerUrl); console.log('โœ… Debug session active. Chrome DevTools connected.'); console.log('๐Ÿ” Monitoring for errors...\n'); // Keep process alive process.on('SIGINT', () => this.cleanup()); process.on('SIGTERM', () => this.cleanup()); } catch (error) { console.error('โŒ Failed to start debug session:', error.message); await this.cleanup(); process.exit(1); } } // Cleanup function async cleanup() { console.log('\n๐Ÿ›‘ Shutting down debug session...'); if (this.cdp) { await this.cdp.close(); } if (this.chrome) { await this.chrome.kill(); } if (this.devServer) { this.devServer.kill(); } console.log('โœ… Cleanup complete'); process.exit(0); } } module.exports = DevWithDebug;