// Lightweight static file server + editor API for saving maps.js // Used in production Docker container const http = require('http'); const fs = require('fs'); const path = require('path'); const PORT = process.env.PORT || 80; const STATIC_DIR = path.join(__dirname, 'public'); const MAPS_FILE = path.join(STATIC_DIR, 'js', 'world', 'maps.js'); const MIME = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.woff2': 'font/woff2', }; const server = http.createServer((req, res) => { // CORS headers for editor res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } // === API: GET /api/maps — read maps.js source === if (req.method === 'GET' && req.url === '/api/maps') { fs.readFile(MAPS_FILE, 'utf-8', (err, data) => { if (err) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Failed to read maps.js' })); return; } res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ content: data })); }); return; } // === API: PUT /api/maps — write maps.js source === if (req.method === 'PUT' && req.url === '/api/maps') { let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', () => { try { const { content } = JSON.parse(body); if (!content || typeof content !== 'string') { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Missing content field' })); return; } // Backup before overwrite const backup = MAPS_FILE + '.bak'; if (fs.existsSync(MAPS_FILE)) { fs.copyFileSync(MAPS_FILE, backup); } fs.writeFileSync(MAPS_FILE, content, 'utf-8'); console.log(`[server] maps.js saved (${content.length} bytes)`); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true, bytes: content.length })); } catch (e) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message })); } }); return; } // === API: GET /api/maps/json — parse current map data as JSON === if (req.method === 'GET' && req.url === '/api/maps/json') { fs.readFile(MAPS_FILE, 'utf-8', (err, data) => { if (err) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Failed to read maps.js' })); return; } // Extract JSON-serializable data from the JS source // This is a best-effort parser for the generated maps.js format res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ source: data })); }); return; } // === Static file serving === let filePath = path.join(STATIC_DIR, req.url === '/' ? 'index.html' : req.url); // Prevent directory traversal if (!filePath.startsWith(STATIC_DIR)) { res.writeHead(403); res.end('Forbidden'); return; } const ext = path.extname(filePath).toLowerCase(); const contentType = MIME[ext] || 'application/octet-stream'; fs.readFile(filePath, (err, data) => { if (err) { if (err.code === 'ENOENT') { res.writeHead(404); res.end('Not found'); } else { res.writeHead(500); res.end('Server error'); } return; } // Cache static assets if (ext === '.png' || ext === '.jpg' || ext === '.woff2') { res.setHeader('Cache-Control', 'public, max-age=86400'); } res.writeHead(200, { 'Content-Type': contentType }); res.end(data); }); }); server.listen(PORT, () => { console.log(`[server] Logic Gates running on port ${PORT}`); console.log(`[server] Static: ${STATIC_DIR}`); console.log(`[server] Maps file: ${MAPS_FILE}`); });