#!/usr/bin/env node /** * 文档站本地预览服务 * * 站点是按 /ny-exam-platform 子路径构建的(base: '/ny-exam-platform/'), * 直接把 dist 目录当根来跑静态服务会导致所有资源 404。 * 这里做一层前缀重写:把 /ny-exam-platform/ 映射到 dist 目录下的 , * 根路径 / 自动跳转到 /ny-exam-platform/。 * * 用法: * node tools/docs-preview.js [端口] [dist目录] * node tools/docs-preview.js 5199 docs/.vitepress/dist-final */ 'use strict'; const http = require('http'); const fs = require('fs'); const path = require('path'); const PORT = parseInt(process.argv[2], 10) || 5199; const DIST = path.resolve(process.argv[3] || path.join(__dirname, '..', 'docs', '.vitepress', 'dist-final')); const BASE = (process.env.DOCS_BASE || '/ny-exam-platform').replace(/\/+$/, ''); const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json; charset=utf-8', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.txt': 'text/plain; charset=utf-8', '.map': 'application/json; charset=utf-8', }; if (!fs.existsSync(DIST)) { console.error('[docs-preview] 找不到构建产物:' + DIST); console.error('请先构建:DOCS_OUT=.vitepress/dist-final npx vitepress build .'); process.exit(1); } /** 把 URL 中的前缀剥掉,得到 dist 内的相对路径 */ function toRel(urlPath) { let p = decodeURIComponent(urlPath.split('?')[0].split('#')[0]); if (p === BASE || p === BASE + '/') return '/index.html'; if (p.startsWith(BASE + '/')) p = p.slice(BASE.length); return p; } /** 依次尝试:原样 / .html / 目录下 index.html,模拟静态托管的行为 */ function resolveFile(rel) { const clean = path.normalize(rel).replace(/^([/\\])+/, ''); const full = path.join(DIST, clean); if (full.indexOf(DIST) !== 0) return null; // 防目录穿越 const candidates = [full, full + '.html', path.join(full, 'index.html')]; for (const c of candidates) { try { if (fs.existsSync(c) && fs.statSync(c).isFile()) return c; } catch (e) { /* 忽略 */ } } return null; } const server = http.createServer((req, res) => { const urlPath = req.url || '/'; // 根路径 / 直接跳到子路径首页 if (urlPath === '/' || urlPath === '') { res.writeHead(302, { Location: BASE + '/' }); res.end(); return; } const rel = toRel(urlPath); const file = resolveFile(rel); if (!file) { res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('404 Not Found: ' + urlPath); return; } const ext = path.extname(file).toLowerCase(); res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream', 'Cache-Control': 'no-cache', }); fs.createReadStream(file).pipe(res); }); server.listen(PORT, '127.0.0.1', () => { console.log('[docs-preview] 文档站已启动'); console.log(' 访问地址:http://127.0.0.1:' + PORT + BASE + '/'); console.log(' 构建目录:' + DIST); }); server.on('error', e => { console.error('[docs-preview] 启动失败:' + e.message); process.exit(1); });