fix(preview): 流式文件服务与 HEAD 跳读;静默开关;redirect 转义;argv 缺值报错

复审四项:
- serveFile 改 createReadStream 直 pipe(Content-Length 取自先行 stat)——
  64MB 文件 RSS 平坦(冒烟实测 -53MB 增长,此前整读驻留);HEAD 只发头
  零读盘;流错误掐断连接不崩进程;头纪律抽 applyHeaders 唯一出口。
- createPreviewServer 增 quiet 选项(缺省 verbose);sf.ts preview:start
  与 server.mjs 直跑入口各加 --quiet;CLI 透传经子进程冒烟验证。
- redirect 迁移页内嵌 location 时 HTML 转义(</&/"——Location 头原样)。
- parseCliArgs 末尾缺值 flag 显式报错(不再静默 undefined 漏成默认值)。

测试 12→13(新增 quiet 开关用例;用例 12 增 --quiet stdout 断言):
根套件 380 + 主题 26 = 406 全绿;typecheck/lint 干净。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-22 13:58:15 +08:00
parent 7194d1bcef
commit 5f78dde9ee
4 changed files with 95 additions and 19 deletions
+30 -3
View File
@@ -15,7 +15,9 @@
// 9. 目录列表禁用(dist 子目录请求 → 404,不出文件清单)
// 10. HEAD 方法支持(200 同头无 body)
// 11. CLI:--host 外值拒绝(校验先于 DB 访问——tsx 直跑 sf.ts)
// 12. CLI happy path:真实 job 子进程全链(job 定位 → manifest 读 → 监听可服务 → 关停)
// 12. CLI happy path:真实 job 子进程全链(job 定位 → manifest 读 → 监听可服务 → 关停;
// --quiet 透传——子进程 stdout 无逐请求访问日志)
// 13. quiet 开关:quiet:true 静默逐请求日志;缺省(verbose)逐请求一行 stdout
import { execFile, spawn } from 'node:child_process'
import { createHash } from 'node:crypto'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
@@ -23,7 +25,7 @@ import { request } from 'node:http'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
import type { RouteManifest, RouteManifestRoute } from '@sf/contracts'
import { createBuildJob } from '../../apps/api/src/modules/build/build.service.js'
import { createImportJob } from '../../apps/api/src/modules/import/import.service.js'
@@ -353,7 +355,7 @@ describe('预览服务器(真实构建产物 + 手构 manifest 扩展)', ()
const jobId = manifest!.build_job_id
const child = spawn(
process.execPath,
[TSX_CLI, join(REPO_ROOT, 'scripts', 'sf.ts'), 'preview:start', '--build', jobId, '--port', '0'],
[TSX_CLI, join(REPO_ROOT, 'scripts', 'sf.ts'), 'preview:start', '--build', jobId, '--port', '0', '--quiet'],
{ cwd: REPO_ROOT, stdio: ['ignore', 'pipe', 'pipe'] },
)
let stdout = ''
@@ -379,6 +381,9 @@ describe('预览服务器(真实构建产物 + 手构 manifest 扩展)', ()
expect(res.status).toBe(200)
expect(res.headers.get('x-robots-tag')).toBe('noindex, nofollow')
expect(await res.text()).toStrictEqual(await readFile(join(distDir, 'index.html'), 'utf8'))
// --quiet 透传:服务过请求后子进程 stdout 仍无逐请求访问日志行
await new Promise((r) => setTimeout(r, 500))
expect(stdout).not.toMatch(/\[preview\] (GET|HEAD)/)
} finally {
child.kill()
await new Promise<void>((resolveP) => {
@@ -390,4 +395,26 @@ describe('预览服务器(真实构建产物 + 手构 manifest 扩展)', ()
})
}
})
it('13. quiet 开关:quiet:true 静默逐请求日志;缺省 verbose 逐请求一行', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
try {
const previewLines = (): number => logSpy.mock.calls.filter((c) => String(c[0]).includes('[preview] ')).length
// quiet 实例:请求零访问日志
const quietServer = createPreviewServer({ distDir, routeManifest: manifest!, port: 0, quiet: true })
const addr = await quietServer.start()
try {
expect((await fetch(`http://127.0.0.1:${addr.port}/`)).status).toBe(200)
expect(previewLines()).toBe(0)
} finally {
await quietServer.stop()
}
// 缺省 verbose 对照(beforeAll 主服务):请求恰一行访问日志
const before = previewLines()
await get('/')
expect(previewLines()).toBe(before + 1)
} finally {
logSpy.mockRestore()
}
})
})