UNPKG

advanced-live-server-installer

Version:

Auto-installer for Advanced Live Server VS Code Extension

586 lines 26.4 kB
"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 (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __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 }); exports.LiveServer = void 0; const express_1 = __importDefault(require("express")); const http = __importStar(require("http")); const https = __importStar(require("https")); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const WebSocket = __importStar(require("ws")); const chokidar = __importStar(require("chokidar")); const mime = __importStar(require("mime-types")); const cors_1 = __importDefault(require("cors")); const http_proxy_middleware_1 = require("http-proxy-middleware"); const events_1 = require("events"); const child_process_1 = require("child_process"); class LiveServer extends events_1.EventEmitter { constructor(rootPath, config, outputChannel) { super(); this.server = null; this.wss = null; this.watcher = null; this.clients = new Set(); this.collabClients = new Set(); this.collabDocs = new Map(); this.isServerRunning = false; this.hmrProcess = null; this.hmrProxyTarget = null; this.rootPath = rootPath; this.config = config; this.outputChannel = outputChannel; // Debug: Log the configuration received by the server this.outputChannel.appendLine(`🔍 LiveServer constructor - Received config:`); this.outputChannel.appendLine(` Port: ${this.config.port}`); this.outputChannel.appendLine(` HTTPS: ${this.config.https}`); this.outputChannel.appendLine(` SPA: ${this.config.spa}`); this.outputChannel.appendLine(` OpenBrowser: ${this.config.openBrowser}`); this.outputChannel.appendLine(` ShowOverlay: ${this.config.showOverlay}`); this.app = (0, express_1.default)(); this.setupMiddleware(); this.setupRoutes(); } setupMiddleware() { // Add request logging middleware this.app.use((req, res, next) => { this.outputChannel.appendLine(`🌐 Incoming request: ${req.method} ${req.path}`); next(); }); // CORS this.app.use((0, cors_1.default)()); // Proxy middleware if (this.config.proxy) { Object.entries(this.config.proxy).forEach(([path, target]) => { this.app.use(path, (0, http_proxy_middleware_1.createProxyMiddleware)({ target, changeOrigin: true, logLevel: 'silent', })); }); } // Handle all static files through our custom route handlers this.outputChannel.appendLine(`🔧 Static file serving disabled - all files handled by custom routes`); } setupRoutes() { // Handle root path specifically this.app.get('/', (req, res) => { const indexPath = path.join(this.rootPath, 'index.html'); this.outputChannel.appendLine(`🔍 Root path requested, checking for: ${indexPath}`); if (fs.existsSync(indexPath)) { this.outputChannel.appendLine(`✅ Found index.html, serving...`); this.serveFile(indexPath, res); } else { this.outputChannel.appendLine(`❌ index.html not found at: ${indexPath}`); this.serve404(res); } }); // Main route handler for all other paths this.app.get('*', (req, res) => { const requestPath = req.path; const fullPath = path.join(this.rootPath, requestPath); this.outputChannel.appendLine(`🔍 Request: ${requestPath} -> ${fullPath}`); this.outputChannel.appendLine(`🔍 File exists check: ${fs.existsSync(fullPath)}`); // Check if file exists if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) { this.outputChannel.appendLine(`✅ File found, serving: ${fullPath}`); this.serveFile(fullPath, res); } else { this.outputChannel.appendLine(`❌ File not found: ${fullPath}`); // Check if it's a directory and has an index.html if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) { const dirIndexPath = path.join(fullPath, 'index.html'); if (fs.existsSync(dirIndexPath)) { this.outputChannel.appendLine(`✅ Found index.html in directory, serving: ${dirIndexPath}`); this.serveFile(dirIndexPath, res); return; } } // SPA fallback or 404 if (this.config.spa) { this.outputChannel.appendLine(`📱 SPA mode enabled, serving index.html for: ${requestPath}`); this.serveSPA(res); } else { this.outputChannel.appendLine(`❌ File not found and SPA mode disabled: ${requestPath}`); this.serve404(res); } } }); } serveFile(filePath, res) { const ext = path.extname(filePath); const contentType = mime.lookup(ext) || 'application/octet-stream'; this.outputChannel.appendLine(`📄 Serving file: ${filePath} (${contentType})`); res.setHeader('Content-Type', contentType); // Add aggressive cache-busting headers for ALL files res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate, private'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); res.setHeader('Last-Modified', new Date().toUTCString()); res.setHeader('ETag', `"${Date.now()}"`); // Inject reload script for HTML files if (ext === '.html') { let content = fs.readFileSync(filePath, 'utf8'); // Force inject reload script for debugging this.outputChannel.appendLine(`🔧 Forcing reload script injection for: ${filePath}`); content = require('../client/reload-script').injectReloadScript(content, this.config.showOverlay); // Debug: Check if script was actually injected if (content.includes('advanced-live-server-reload')) { this.outputChannel.appendLine(`✅ Reload script successfully injected`); } else { this.outputChannel.appendLine(`❌ Reload script NOT found in injected content`); } this.outputChannel.appendLine(`✅ HTML file served with reload script: ${filePath}`); res.send(content); } else { this.outputChannel.appendLine(`✅ Static file served: ${filePath}`); res.sendFile(filePath); } } serveSPA(res) { const indexPath = path.join(this.rootPath, 'index.html'); if (fs.existsSync(indexPath)) { let content = fs.readFileSync(indexPath, 'utf8'); this.outputChannel.appendLine(`🔧 Injecting reload script for SPA fallback`); content = require('../client/reload-script').injectReloadScript(content, this.config.showOverlay); res.setHeader('Content-Type', 'text/html'); res.send(content); } else { this.serve404(res); } } serve404(res) { res.status(404).send(` <!DOCTYPE html> <html> <head> <title>404 - File Not Found</title> <style> body { font-family: Arial, sans-serif; text-align: center; padding: 50px; } .error { color: #e74c3c; font-size: 72px; margin-bottom: 20px; } .message { font-size: 18px; color: #7f8c8d; } </style> </head> <body> <div class="error">404</div> <div class="message">File not found</div> <p>Advanced Live Server</p> </body> </html> `); } async start() { try { this.outputChannel.appendLine(`🚀 Starting Advanced Live Server...`); this.outputChannel.appendLine(`📁 Root path: ${this.rootPath}`); this.outputChannel.appendLine(`🔧 Port: ${this.config.port}`); this.outputChannel.appendLine(`🔒 HTTPS: ${this.config.https ? 'Enabled' : 'Disabled'}`); this.outputChannel.appendLine(`🌐 Auto-open browser: ${this.config.openBrowser ? 'Enabled' : 'Disabled'}`); this.outputChannel.appendLine(`📱 SPA mode: ${this.config.spa ? 'Enabled' : 'Disabled'}`); // HMR Adapter: auto-detect and start dev server if needed const projectType = this.config.projectType; if (projectType && ['vite', 'snowpack', 'webpack'].includes(projectType)) { await this.startHMRAdapter(projectType); } // Create server (HTTP or HTTPS) if (this.config.https) { this.outputChannel.appendLine(`🔒 Generating HTTPS certificates...`); const certs = await this.generateCertificates(); this.outputChannel.appendLine(`✅ HTTPS certificates generated successfully`); this.server = https.createServer({ key: (certs.key ?? ''), cert: (certs.cert ?? ''), }, this.app); this.outputChannel.appendLine(`🔒 HTTPS server created`); } else { this.server = http.createServer(this.app); this.outputChannel.appendLine(`🌐 HTTP server created`); } // Start server this.outputChannel.appendLine(`🚀 Starting server on port ${this.config.port}...`); await new Promise((resolve, reject) => { this.server.listen(this.config.port, () => { this.isServerRunning = true; this.outputChannel.appendLine(`✅ Server started successfully on port ${this.config.port}`); resolve(); }); this.server.on('error', error => { this.outputChannel.appendLine(`❌ Server startup error: ${error.message}`); reject(error); }); }); // Setup HMR proxy if needed if (this.hmrProxyTarget) { this.app.use('/', (0, http_proxy_middleware_1.createProxyMiddleware)({ target: this.hmrProxyTarget, changeOrigin: true, ws: true, logLevel: 'silent', onProxyReq: () => { // Optionally log or modify requests }, })); this.outputChannel.appendLine(`🔗 HMR proxy enabled: ${this.hmrProxyTarget}`); } // Setup WebSocket server this.setupWebSocket(); // Setup file watcher this.setupFileWatcher(); // Emit started event this.emit('started', this.config.port, this.config.https); } catch (error) { this.outputChannel.appendLine(`❌ Failed to start server: ${error}`); this.emit('error', `Failed to start server: ${error}`); throw error; } } async startHMRAdapter(projectType) { // Check if dev server is running, otherwise start it let devPort = 5173; // Vite default let devCmd = ''; let devArgs = []; let devTarget = ''; if (projectType === 'vite') { devCmd = 'npx'; devArgs = ['vite']; devPort = 5173; devTarget = `http://localhost:${devPort}`; } else if (projectType === 'snowpack') { devCmd = 'npx'; devArgs = ['snowpack', 'dev']; devPort = 8080; devTarget = `http://localhost:${devPort}`; } else if (projectType === 'webpack') { devCmd = 'npx'; devArgs = ['webpack', 'serve']; devPort = 8080; devTarget = `http://localhost:${devPort}`; } // Check if port is open const isRunning = await this.checkPortOpen(devPort); if (!isRunning) { this.outputChannel.appendLine(`🚀 Starting ${projectType} dev server...`); this.hmrProcess = (0, child_process_1.spawn)(devCmd, devArgs, { cwd: this.rootPath, shell: true, stdio: 'inherit', }); // Wait for server to start await this.waitForPort(devPort, 15000); } this.hmrProxyTarget = devTarget; } checkPortOpen(port) { return new Promise(resolve => { const net = require('net'); const tester = net.createConnection({ port }, () => { tester.end(); resolve(true); }); tester.on('error', () => resolve(false)); }); } waitForPort(port, timeout) { return new Promise((resolve, reject) => { const start = Date.now(); const check = async () => { if (await this.checkPortOpen(port)) { return resolve(); } if (Date.now() - start > timeout) { return reject(new Error('Dev server did not start in time')); } setTimeout(check, 500); }; check(); }); } setupWebSocket() { if (!this.server) { this.outputChannel.appendLine(`❌ Cannot setup WebSocket: server is null`); return; } this.wss = new WebSocket.Server({ server: this.server }); this.outputChannel.appendLine(`🔧 WebSocket server created`); this.wss.on('connection', (ws) => { this.clients.add(ws); this.collabClients.add(ws); this.outputChannel.appendLine(`🔗 Client connected (${this.clients.size} total) - WebSocket readyState: ${ws.readyState}`); ws.on('message', data => { try { const msg = JSON.parse(data.toString()); this.outputChannel.appendLine(`📨 Received message from client: ${JSON.stringify(msg)}`); if (msg && msg.type && msg.channel === 'collab') { this.handleCollabMessage(ws, msg); } } catch (e) { this.outputChannel.appendLine(`❌ WebSocket message error: ${e}`); } }); ws.on('close', (code, reason) => { this.clients.delete(ws); this.collabClients.delete(ws); this.outputChannel.appendLine(`🔌 Client disconnected (${this.clients.size} total) - Code: ${code}, Reason: ${reason}`); }); ws.on('error', error => { this.outputChannel.appendLine(`❌ WebSocket error: ${error}`); }); }); this.wss.on('error', error => { this.outputChannel.appendLine(`❌ WebSocket server error: ${error}`); }); } handleCollabMessage(ws, msg) { // msg: { type, docId, userId, ... } const { type, docId, userId, content, cursor, comment } = msg; if (!docId) { return; } if (!this.collabDocs.has(docId)) { this.collabDocs.set(docId, { content: '', cursors: {}, comments: [] }); } const doc = this.collabDocs.get(docId); if (type === 'doc-sync') { doc.content = content; this.broadcastCollab({ type: 'doc-sync', docId, userId, content }, ws); } else if (type === 'cursor-sync') { doc.cursors[userId] = cursor; this.broadcastCollab({ type: 'cursor-sync', docId, userId, cursor }, ws); } else if (type === 'comment-add') { doc.comments.push(comment); this.broadcastCollab({ type: 'comment-add', docId, userId, comment }, ws); } else if (type === 'comment-remove') { doc.comments = doc.comments.filter((c) => c.id !== comment.id); this.broadcastCollab({ type: 'comment-remove', docId, userId, comment }, ws); } else if (type === 'get-state') { ws.send(JSON.stringify({ channel: 'collab', type: 'state', docId, content: doc.content, cursors: doc.cursors, comments: doc.comments, })); } } broadcastCollab(msg, exceptWs) { const data = JSON.stringify({ ...msg, channel: 'collab' }); this.collabClients.forEach(client => { if (client !== exceptWs && client.readyState === WebSocket.OPEN) { client.send(data); } }); } setupFileWatcher() { this.outputChannel.appendLine(`🔧 Setting up file watcher for: ${this.rootPath}`); this.outputChannel.appendLine(`🔧 Ignore patterns: ${JSON.stringify(this.config.ignorePatterns)}`); this.watcher = chokidar.watch(this.rootPath, { ignored: this.config.ignorePatterns, persistent: true, ignoreInitial: true, awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 100 }, usePolling: true, interval: 100 }); this.watcher.on('ready', () => { this.outputChannel.appendLine(`✅ File watcher ready`); }); this.watcher.on('change', filePath => { this.outputChannel.appendLine(`📝 File change detected: ${filePath}`); this.handleFileChange(filePath); }); this.watcher.on('add', filePath => { this.outputChannel.appendLine(`📁 File added: ${path.relative(this.rootPath, filePath)}`); }); this.watcher.on('unlink', filePath => { this.outputChannel.appendLine(`🗑️ File deleted: ${path.relative(this.rootPath, filePath)}`); }); this.watcher.on('error', error => { this.outputChannel.appendLine(`❌ File watcher error: ${error}`); }); } handleFileChange(filePath) { const relativePath = path.relative(this.rootPath, filePath); const ext = path.extname(filePath); this.outputChannel.appendLine(`📝 File changed: ${relativePath} (${ext})`); this.outputChannel.appendLine(`📊 Active clients before notification: ${this.clients.size}`); this.emit('fileChanged', relativePath); // Force a small delay to ensure file is fully written and client is connected setTimeout(() => { // Notify all connected clients with timestamp to force reload this.notifyClients({ type: 'reload', file: relativePath, extension: ext, timestamp: Date.now(), force: true }); }, 200); // Increased delay to ensure client is fully connected } notifyClients(message) { const messageStr = JSON.stringify(message); this.outputChannel.appendLine(`📤 Notifying ${this.clients.size} clients: ${messageStr}`); let clientIndex = 0; this.clients.forEach(client => { clientIndex++; if (client.readyState === WebSocket.OPEN) { client.send(messageStr); this.outputChannel.appendLine(`✅ Message sent to client ${clientIndex}`); } else { this.outputChannel.appendLine(`❌ Client ${clientIndex} not ready (state: ${client.readyState})`); } }); } async generateCertificates() { // Try to use mkcert for trusted local certificates if available const fs = require('fs'); const path = require('path'); const os = require('os'); const { execSync } = require('child_process'); const certDir = path.join(os.homedir(), '.advanced-live-server', 'certs'); const keyPath = path.join(certDir, 'localhost-key.pem'); const certPath = path.join(certDir, 'localhost-cert.pem'); // Check if mkcert is installed let mkcertAvailable = false; try { execSync('mkcert --version', { stdio: 'ignore' }); mkcertAvailable = true; } catch { mkcertAvailable = false; } if (mkcertAvailable) { this.outputChannel.appendLine(`🔒 Attempting to use mkcert for trusted certificates...`); // Ensure cert directory exists if (!fs.existsSync(certDir)) { fs.mkdirSync(certDir, { recursive: true }); this.outputChannel.appendLine(`📁 Created directory: ${certDir}`); } // Generate certs if not present if (!fs.existsSync(keyPath) || !fs.existsSync(certPath)) { try { this.outputChannel.appendLine(`🔒 Generating new mkcert certificates...`); execSync(`mkcert -key-file "${keyPath}" -cert-file "${certPath}" localhost 127.0.0.1 ::1`, { stdio: 'inherit' }); this.outputChannel.appendLine(`✅ mkcert certificates generated successfully.`); } catch (e) { const errorMessage = e instanceof Error ? e.message : String(e); this.outputChannel.appendLine(`❌ mkcert failed to generate certificates: ${errorMessage}`); this.outputChannel.appendLine(`🔒 Falling back to self-signed certificate.`); } } else { this.outputChannel.appendLine(`🔒 mkcert certificates already exist at: ${certDir}`); } if (fs.existsSync(keyPath) && fs.existsSync(certPath)) { this.outputChannel.appendLine(`🔒 Using trusted local certificate from mkcert.`); return { key: fs.readFileSync(keyPath, 'utf8') || '', cert: fs.readFileSync(certPath, 'utf8') || '', }; } } else { this.outputChannel.appendLine(`🔒 mkcert is not installed. Falling back to self-signed certificate.`); } // Fallback: use self-signed certificate this.outputChannel.appendLine(`🔒 Using self-signed certificate. For trusted certs, install mkcert.`); const selfsigned = require('selfsigned'); const attrs = [{ name: 'commonName', value: 'localhost' }]; const pems = selfsigned.generate(attrs, { days: 365 }); return { key: pems.private || '', cert: pems.cert || '', }; } async stop() { this.isServerRunning = false; // Close WebSocket server if (this.wss) { this.wss.close(); this.wss = null; } // Close file watcher if (this.watcher) { await this.watcher.close(); this.watcher = null; } // Close HTTP/HTTPS server if (this.server) { await new Promise(resolve => { this.server.close(() => resolve()); }); this.server = null; } // Clear clients this.clients.clear(); this.emit('stopped'); } isRunning() { return this.isServerRunning; } getServerInfo() { if (!this.isServerRunning) { return null; } const protocol = this.config.https ? 'https' : 'http'; const url = `${protocol}://localhost:${this.config.port}`; // Debug: Log the server info being generated this.outputChannel.appendLine(`🔍 getServerInfo - Generating URL:`); this.outputChannel.appendLine(` Config HTTPS: ${this.config.https}`); this.outputChannel.appendLine(` Protocol: ${protocol}`); this.outputChannel.appendLine(` Port: ${this.config.port}`); this.outputChannel.appendLine(` Final URL: ${url}`); return { url: url, port: this.config.port, https: this.config.https, }; } } exports.LiveServer = LiveServer; //# sourceMappingURL=live-server.js.map