// 零依赖本地热重载静态服务器 — pangolin 原型单源预览 // 用法: node design/prototype/serve.mjs [port] 默认 5180 // 评审给 URL,不截图(ds-flow L3)。 import http from 'node:http'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { spawn } from 'node:child_process'; const ROOT = path.dirname(fileURLToPath(import.meta.url)); const PORT = Number(process.argv[2]) || 5180; const MIME = { '.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.woff2': 'font/woff2', '.otf': 'font/otf', '.ttf': 'font/ttf', }; const clients = new Set(); const RELOAD_SNIPPET = `\n\n`; const server = http.createServer((req, res) => { if (req.url === '/__reload') { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }); res.write('retry: 500\n\n'); clients.add(res); req.on('close', () => clients.delete(res)); return; } let urlPath = decodeURIComponent(req.url.split('?')[0]); if (urlPath === '/') urlPath = '/index.html'; const filePath = path.join(ROOT, urlPath); if (!filePath.startsWith(ROOT)) { res.writeHead(403); res.end('forbidden'); return; } fs.readFile(filePath, (err, data) => { if (err) { res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('

404

'); return; } const ext = path.extname(filePath).toLowerCase(); const mime = MIME[ext] || 'application/octet-stream'; const noCache = { 'Cache-Control': 'no-store, no-cache, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0' }; if (ext === '.html') { const html = data.toString('utf8').replace('', RELOAD_SNIPPET + ''); res.writeHead(200, { 'Content-Type': mime, ...noCache }); res.end(html); } else { res.writeHead(200, { 'Content-Type': mime, ...noCache }); res.end(data); } }); }); // 设计系统守门:启动 + 每次改动自动跑 check-ds.mjs(Phase 5 建立后生效)。 const CHECK_DS = path.join(ROOT, 'tools', 'check-ds.mjs'); function runDsCheck() { if (!fs.existsSync(CHECK_DS)) return; // Phase 5 前无闸,静默跳过 const p = spawn(process.execPath, [CHECK_DS], { cwd: ROOT }); let out = ''; p.stdout.on('data', d => out += d); p.stderr.on('data', d => out += d); p.on('close', code => { const t = new Date().toLocaleTimeString(); if (code === 0) console.log(`\x1b[32m[设计系统 ✓ ${t}] 颜色全部走 token、变量均已定义\x1b[0m`); else console.log(`\x1b[31m[设计系统 ✗ ${t}] 存在违规 —— 运行 node tools/check-ds.mjs 看详情\x1b[0m`); }); } let debounce; fs.watch(ROOT, { recursive: true }, (_ev, file) => { if (file && file.endsWith('serve.mjs')) return; clearTimeout(debounce); debounce = setTimeout(() => { for (const c of clients) c.write('data: reload\n\n'); if (file && /\.(html|css|js)$/.test(file)) runDsCheck(); }, 80); }); server.listen(PORT, () => { console.log(`prototype live at http://localhost:${PORT}/ (watching ${ROOT})`); runDsCheck(); });