@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
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 () {
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