import {CustomMDXProcessor} from './processor.js'; import fs from 'fs'; import fsPromises from 'fs/promises'; import path from 'path'; import yaml from 'js-yaml'; import {WebSocket, WebSocketServer} from 'ws'; import http from 'http'; import {program} from 'commander'; import crypto from 'crypto'; const processor = new CustomMDXProcessor(); let latestHtml = ''; let latestContent = ''; let lastFileHash = ''; function hashFile(content: string): string { return crypto.createHash('md5').update(content).digest('hex'); } const watcherJs = ``; const server = http.createServer(async (req, res) => { // Set CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Request-Method', '*'); res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET'); res.setHeader('Access-Control-Allow-Headers', '*'); if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } if (req.url === '/') { res.writeHead(200, {'Content-Type': 'text/html'}); res.end(latestHtml); } else { // Serve static files try { const filePath = path.join("./", req.url as any); const data = await fsPromises.readFile(filePath); const ext = path.extname(filePath); const contentType = getContentType(ext); res.writeHead(200, {'Content-Type': contentType}); res.end(data); } catch (err: any) { console.error(err); if (err.code === 'ENOENT') { res.writeHead(404, {'Content-Type': 'text/plain'}); res.end('404 Not Found'); } else { res.writeHead(500, {'Content-Type': 'text/plain'}); res.end('500 Internal Server Error'); } } } function getContentType(extension: string) { switch (extension) { case '.html': return 'text/html'; case '.css': return 'text/css'; case '.js': return 'text/javascript'; case '.json': return 'application/json'; case '.png': return 'image/png'; case '.jpg': case '.jpeg': return 'image/jpeg'; default: return 'text/plain'; } } }); const wss = new WebSocketServer({server}); async function processFile(filePath: string) { const content = await fsPromises.readFile(filePath, 'utf-8'); const currentHash = hashFile(content); if (currentHash === lastFileHash) { throw new Error(`File didn't change change.`); } lastFileHash = currentHash; const {frontmatter, markdown} = extractFrontmatter(content); const config = await loadConfig(frontmatter, path.dirname(filePath)); config.head ||= ''; config.head += watcherJs; latestContent = await processor.process(markdown, config); latestHtml = await processor.wrapHtml(latestContent); } async function watchAndProcess(watchPath: string) { const stats = await fsPromises.stat(watchPath); if (stats.isFile() && path.extname(watchPath) === '.md') { watchFile(watchPath); } else { console.error(`${watchPath} is neither a directory nor a Markdown file.`); } } function watchFile(filePath: string) { fs.watch(filePath, async (eventType) => { if (eventType === 'change') { try { await processFile(filePath); wss.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify( {type: "reload", html: latestHtml} )); } }); console.log(`Updated: ${filePath}`); } catch (error) { console.error(`Error processing ${filePath}:`, error); } } }); } function extractFrontmatter(content: string) { const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); if (match) { return { frontmatter: yaml.load(match[1]) as Record, markdown: match[2], }; } return {frontmatter: {}, markdown: content}; } async function loadConfig(frontmatter: Record, inputDir: string) { let config: Record = {}; const configPath = path.join(inputDir, 'config.yml'); if (await fsPromises.access(configPath).then(() => true).catch(() => false)) { const globalConfig = yaml.load(await fsPromises.readFile(configPath, 'utf-8')) as Record; config = {...globalConfig, ...frontmatter}; } else { config = frontmatter; } return config; } export async function live(inputPath: string) { // Initial processing await processFile(inputPath); // Start watching for changes await watchAndProcess(inputPath); server.listen(8080, "0.0.0.0", () => { console.log('See it live at: http://localhost:8080'); }); }