// 本地预览服务器(Plan 4 Task 7,V2 spec §12.2):路由驱动的静态 dist 服务—— // 只读 /jobs//dist + route-manifest.json 两个输入,不查库、 // 不写盘、零新依赖(node:http)。行为面: // - 匹配器:URL 解析 pathname + search;query_policy 处理——exact 行(query_key 非空) // 以 `pathname+search` 全串匹配且优先于 ignore 行;ignore 行剥 query 按 path 匹配。 // html/asset → target_file 文件服务(**流式**:createReadStream 直pipe,Content-Length // 取自先行 stat——大文件不驻留内存;HEAD 只发头零读盘;manifest content_type 优先, // 缺席走扩展名 MIME 表,未知扩展 application/octet-stream);redirect → status+Location // (**头域 ASCII 化**:manifest location 含非 ASCII 码位(如全角括号/间隔号)时按 // UTF-8 百分号编码后发头——RFC 9110 头域是 ASCII,原样 setHeader 会 ERR_INVALID_CHAR // 崩成 500 或被对端按 latin1 误解码;manifest 原串保持权威,线上形态是派生面); // gone → 410;dynamic/unresolved → 404(静态产物无对应文件);无匹配 → 404(dist/ // 404.html 自定义页)。 // - 安全:回环绑定(host 仅 127.0.0.1/localhost,库级即拒);路径穿越预拒(反斜杠/ // 编码形态/.. 段)+ target_file resolve 后必须在 dist 内(lexical + realpath 双检); // 目录即 404(禁目录列表);仅 GET/HEAD(其余 405)。 // - 头纪律:全响应 X-Robots-Tag: noindex, nofollow;text/html 响应附基础 CSP // (default-src 'self'; style-src 'self' 'unsafe-inline'——Astro 内联 scoped 样式放行, // 见常量处注记)。请求日志一行(method/path/status/耗时)到 stdout // (quiet: true 静默)。 // // 本文件刻意保持 .mjs 纯 JavaScript(不进 TS 链);TS 消费方(scripts/sf.ts 与集成 // 测试)的类型面由相邻 server.d.mts 声明供给(NodeNext 解析自动拾取)。 import { createReadStream } from 'node:fs' import { createServer } from 'node:http' import { readFile, realpath, stat } from 'node:fs/promises' import { extname, join, resolve, sep } from 'node:path' import { pathToFileURL } from 'node:url' /** 扩展名 → Content-Type(manifest content_type 缺席时的兜底映射;未列出 → octet-stream) */ const MIME_BY_EXT = new Map([ ['.html', 'text/html'], ['.htm', 'text/html'], ['.css', 'text/css'], ['.js', 'text/javascript'], ['.mjs', 'text/javascript'], ['.json', 'application/json'], ['.xml', 'application/xml'], ['.txt', 'text/plain'], ['.png', 'image/png'], ['.jpg', 'image/jpeg'], ['.jpeg', 'image/jpeg'], ['.gif', 'image/gif'], ['.webp', 'image/webp'], ['.svg', 'image/svg+xml'], ['.ico', 'image/x-icon'], ['.pdf', 'application/pdf'], ['.woff', 'font/woff'], ['.woff2', 'font/woff2'], ]) /** 回环绑定白名单(§12.2:预览永不对外)——外值在服务构造点即拒(先于监听) */ const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost']) const X_ROBOTS_TAG = 'noindex, nofollow' // CSP(Task 8 浏览器验收实证修补):Astro 5 静态产物把 scoped 样式内联为