advanced-live-server-installer
Version:
Auto-installer for Advanced Live Server VS Code Extension
586 lines • 26.4 kB
JavaScript
"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