UNPKG

@mdfriday/foundry

Version:

The core engine of MDFriday. Convert Markdown and shortcodes into fully themed static sites – Hugo-style, powered by TypeScript.

406 lines (398 loc) 14.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 () { 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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ElectronLiveReloadServer = void 0; const http = __importStar(require("http")); const path = __importStar(require("path")); const fs = __importStar(require("fs/promises")); const log_1 = require("../../log"); const log = (0, log_1.getDomainLogger)('web', { component: 'electron-livereload-server' }); class ElectronLiveReloadServer { constructor(config) { this.httpServer = null; this.running = false; this.config = { port: config.port || 8091, host: config.host || 'localhost', livereloadPort: config.livereloadPort || 35729, enableLiveReload: config.enableLiveReload !== false, publicDir: config.publicDir }; // 状态文件放在 publicDir 中 this.stateFilePath = path.join(this.config.publicDir, '.foundry-livereload-state.json'); } async start() { if (this.running) { log.warn('ElectronLiveReloadServer already running'); return; } try { // 启动 HTTP 服务器 await this.startHttpServer(); // 初始化状态文件 if (this.config.enableLiveReload) { await this.initStateFile(); } this.running = true; log.info(`ElectronLiveReloadServer started at ${this.getUrl()}`); } catch (error) { log.error('Failed to start ElectronLiveReloadServer:', error); throw error; } } async stop() { if (!this.running) { return; } try { // 关闭 HTTP 服务器 if (this.httpServer) { await new Promise((resolve) => { this.httpServer.close(() => resolve()); }); this.httpServer = null; } // 清理状态文件 try { await fs.unlink(this.stateFilePath); } catch (error) { // 忽略文件不存在的错误 } this.running = false; log.info('ElectronLiveReloadServer stopped'); } catch (error) { log.error('Error stopping ElectronLiveReloadServer:', error); } } notifyReload(changedFiles) { if (!this.config.enableLiveReload) { return; } const state = { timestamp: Date.now(), command: 'reload', liveCSS: this.shouldLiveReloadCSS(changedFiles), liveImg: this.shouldLiveReloadImages(changedFiles), ...(changedFiles && { changedFiles }) }; // 如果只是单个文件变化,设置具体路径 if (changedFiles && changedFiles.length === 1) { const file = changedFiles[0]; const ext = path.extname(file).toLowerCase(); if (ext === '.css' || ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp'].includes(ext)) { state.path = file; } } // 写入状态文件 this.writeStateFile(state).catch(error => { log.error('Failed to write state file:', error); }); } getUrl() { return `http://${this.config.host}:${this.config.port}`; } isServerRunning() { return this.running; } async initStateFile() { const initialState = { timestamp: Date.now(), command: 'hello' }; await this.writeStateFile(initialState); } async writeStateFile(state) { try { await fs.writeFile(this.stateFilePath, JSON.stringify(state), 'utf8'); } catch (error) { log.error('Failed to write LiveReload state file:', error); } } async startHttpServer() { this.httpServer = http.createServer(async (req, res) => { try { await this.handleHttpRequest(req, res); } catch (error) { log.error('HTTP request error:', error); res.statusCode = 500; res.end('Internal Server Error'); } }); await new Promise((resolve, reject) => { this.httpServer.listen(this.config.port, this.config.host, () => { resolve(); }); this.httpServer.on('error', (error) => { if (error.code === 'EADDRINUSE') { // 端口被占用,尝试下一个端口 this.config.port++; if (this.config.port < 8099) { this.httpServer.listen(this.config.port, this.config.host); } else { reject(new Error('No available ports')); } } else { reject(error); } }); }); } async handleHttpRequest(req, res) { const url = req.url || '/'; // 特殊处理状态文件请求 if (url.startsWith('/.foundry-livereload-state.json')) { try { const content = await fs.readFile(this.stateFilePath, 'utf8'); res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); res.statusCode = 200; res.end(content); return; } catch (error) { if (error.code === 'ENOENT') { res.statusCode = 404; res.end('State file not found'); } else { res.statusCode = 500; res.end('Internal Server Error'); } return; } } let filePath = this.resolveFilePath(url); try { // 检查文件是否存在 const stats = await fs.stat(filePath); if (stats.isDirectory()) { // 尝试查找 index.html const indexPath = path.join(filePath, 'index.html'); try { await fs.stat(indexPath); filePath = indexPath; } catch { // 如果没有 index.html,返回 404 res.statusCode = 404; res.end('Not Found'); return; } } // 读取文件内容 let content = await fs.readFile(filePath); // 设置 Content-Type const contentType = this.getContentType(filePath); res.setHeader('Content-Type', contentType); // 如果是 HTML 文件且启用了 LiveReload,注入 LiveReload 脚本 if (contentType.includes('text/html') && this.config.enableLiveReload) { const htmlContent = content.toString(); const liveReloadScript = this.getLiveReloadScript(); // 在 </body> 标签前插入 LiveReload 脚本 const modifiedHtml = htmlContent.replace(/<\/body>/i, `${liveReloadScript}\n</body>`); content = Buffer.from(modifiedHtml, 'utf8'); } // 设置缓存头(开发环境不缓存) res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); res.statusCode = 200; res.end(content); } catch (error) { if (error.code === 'ENOENT') { res.statusCode = 404; res.end('Not Found'); } else { throw error; } } } resolveFilePath(url) { // 移除查询参数和哈希 const cleanUrl = url.split('?')[0].split('#')[0]; // 解码 URL - 添加错误处理 let decodedUrl; try { decodedUrl = decodeURIComponent(cleanUrl); } catch (error) { log.warn('Failed to decode URL:', cleanUrl, error); decodedUrl = cleanUrl; } // 规范化路径,防止目录遍历攻击 const normalizedPath = path.normalize(decodedUrl).replace(/^(\.\.[\/\\])+/, ''); // 移除前导斜杠 const relativePath = normalizedPath.startsWith('/') ? normalizedPath.slice(1) : normalizedPath; // 使用 path.join 确保跨平台兼容性 const resolvedPath = path.join(this.config.publicDir, relativePath); return resolvedPath; } getContentType(filePath) { const ext = path.extname(filePath).toLowerCase(); const mimeTypes = { '.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'application/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.webp': 'image/webp', '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.eot': 'application/vnd.ms-fontobject', '.xml': 'application/xml; charset=utf-8', '.txt': 'text/plain; charset=utf-8' }; return mimeTypes[ext] || 'application/octet-stream'; } getLiveReloadScript() { return ` <script> (function() { 'use strict'; var stateFilePath = '/.foundry-livereload-state.json'; var storageKey = 'foundry-livereload-last-timestamp'; var lastTimestamp = 0; var pollInterval = 500; // 500ms 轮询间隔 // 从 localStorage 恢复上次的时间戳,避免页面刷新后重复触发 try { var stored = localStorage.getItem(storageKey); if (stored) { lastTimestamp = parseInt(stored, 10) || 0; } } catch (error) { // localStorage 可能不可用,使用默认值 console.warn('LiveReload: localStorage not available, may cause duplicate reloads'); } function checkForReload() { fetch(stateFilePath + '?_t=' + Date.now(), { cache: 'no-cache' }) .then(function(response) { if (!response.ok) { throw new Error('Failed to fetch state file'); } return response.json(); }) .then(function(state) { if (state.timestamp > lastTimestamp) { // 更新时间戳并保存到 localStorage lastTimestamp = state.timestamp; try { localStorage.setItem(storageKey, lastTimestamp.toString()); } catch (error) { // localStorage 写入失败,忽略 } if (state.command === 'reload') { if (state.liveCSS) { // 热更新 CSS reloadCSS(); } else if (state.liveImg) { // 热更新图片 reloadImages(); } else { // 完整页面刷新 window.location.reload(); } } } }) .catch(function(error) { // 静默处理错误,避免控制台噪音 }); } function reloadCSS() { var links = document.querySelectorAll('link[rel="stylesheet"]'); for (var i = 0; i < links.length; i++) { var link = links[i]; var href = link.href; if (href) { var url = new URL(href); url.searchParams.set('_t', Date.now().toString()); link.href = url.toString(); } } console.log('CSS reloaded'); } function reloadImages() { var images = document.querySelectorAll('img'); for (var i = 0; i < images.length; i++) { var img = images[i]; var src = img.src; if (src) { var url = new URL(src); url.searchParams.set('_t', Date.now().toString()); img.src = url.toString(); } } console.log('Images reloaded'); } // 开始轮询 setInterval(checkForReload, pollInterval); // 初始检查 checkForReload(); })(); </script>`; } shouldLiveReloadCSS(changedFiles) { if (!changedFiles) return false; return changedFiles.some(file => path.extname(file).toLowerCase() === '.css'); } shouldLiveReloadImages(changedFiles) { if (!changedFiles) return false; const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp']; return changedFiles.some(file => imageExts.includes(path.extname(file).toLowerCase())); } } exports.ElectronLiveReloadServer = ElectronLiveReloadServer; //# sourceMappingURL=electron-livereload-server.js.map