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:
@@ -29,6 +29,8 @@ export interface PreviewServerOptions {
|
|||||||
host?: string
|
host?: string
|
||||||
/** 监听端口(缺省 4173;0 = 随机可用端口) */
|
/** 监听端口(缺省 4173;0 = 随机可用端口) */
|
||||||
port?: number
|
port?: number
|
||||||
|
/** 静默逐请求访问日志(缺省 false——逐请求一行 stdout) */
|
||||||
|
quiet?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PreviewServerHandle {
|
export interface PreviewServerHandle {
|
||||||
|
|||||||
@@ -3,17 +3,21 @@
|
|||||||
// 不写盘、零新依赖(node:http)。行为面:
|
// 不写盘、零新依赖(node:http)。行为面:
|
||||||
// - 匹配器:URL 解析 pathname + search;query_policy 处理——exact 行(query_key 非空)
|
// - 匹配器:URL 解析 pathname + search;query_policy 处理——exact 行(query_key 非空)
|
||||||
// 以 `pathname+search` 全串匹配且优先于 ignore 行;ignore 行剥 query 按 path 匹配。
|
// 以 `pathname+search` 全串匹配且优先于 ignore 行;ignore 行剥 query 按 path 匹配。
|
||||||
// html/asset → target_file 文件服务(manifest content_type 优先,缺席走扩展名 MIME
|
// html/asset → target_file 文件服务(**流式**:createReadStream 直pipe,Content-Length
|
||||||
// 表,未知扩展 application/octet-stream);redirect → status+Location;gone → 410;
|
// 取自先行 stat——大文件不驻留内存;HEAD 只发头零读盘;manifest content_type 优先,
|
||||||
// dynamic/unresolved → 404(静态产物无对应文件);无匹配 → 404(dist/404.html 自定义页)。
|
// 缺席走扩展名 MIME 表,未知扩展 application/octet-stream);redirect → status+Location;
|
||||||
|
// gone → 410;dynamic/unresolved → 404(静态产物无对应文件);无匹配 → 404(dist/
|
||||||
|
// 404.html 自定义页)。
|
||||||
// - 安全:回环绑定(host 仅 127.0.0.1/localhost,库级即拒);路径穿越预拒(反斜杠/
|
// - 安全:回环绑定(host 仅 127.0.0.1/localhost,库级即拒);路径穿越预拒(反斜杠/
|
||||||
// 编码形态/.. 段)+ target_file resolve 后必须在 dist 内(lexical + realpath 双检);
|
// 编码形态/.. 段)+ target_file resolve 后必须在 dist 内(lexical + realpath 双检);
|
||||||
// 目录即 404(禁目录列表);仅 GET/HEAD(其余 405)。
|
// 目录即 404(禁目录列表);仅 GET/HEAD(其余 405)。
|
||||||
// - 头纪律:全响应 X-Robots-Tag: noindex, nofollow;text/html 响应附基础 CSP
|
// - 头纪律:全响应 X-Robots-Tag: noindex, nofollow;text/html 响应附基础 CSP
|
||||||
// (default-src 'self')。请求日志一行(method/path/status/耗时)到 stdout。
|
// (default-src 'self')。请求日志一行(method/path/status/耗时)到 stdout
|
||||||
|
// (quiet: true 静默)。
|
||||||
//
|
//
|
||||||
// 本文件刻意保持 .mjs 纯 JavaScript(不进 TS 链);TS 消费方(scripts/sf.ts 与集成
|
// 本文件刻意保持 .mjs 纯 JavaScript(不进 TS 链);TS 消费方(scripts/sf.ts 与集成
|
||||||
// 测试)的类型面由相邻 server.d.mts 声明供给(NodeNext 解析自动拾取)。
|
// 测试)的类型面由相邻 server.d.mts 声明供给(NodeNext 解析自动拾取)。
|
||||||
|
import { createReadStream } from 'node:fs'
|
||||||
import { createServer } from 'node:http'
|
import { createServer } from 'node:http'
|
||||||
import { readFile, realpath, stat } from 'node:fs/promises'
|
import { readFile, realpath, stat } from 'node:fs/promises'
|
||||||
import { extname, join, resolve, sep } from 'node:path'
|
import { extname, join, resolve, sep } from 'node:path'
|
||||||
@@ -72,8 +76,13 @@ function contentTypeForExt(file) {
|
|||||||
return MIME_BY_EXT.get(extname(file).toLowerCase()) ?? 'application/octet-stream'
|
return MIME_BY_EXT.get(extname(file).toLowerCase()) ?? 'application/octet-stream'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 统一响应出口:全响应 noindex 头;text/html 附 CSP;HEAD 不写 body(Content-Length 仍按实体设置) */
|
/** HTML 文本转义(redirect 迁移页内嵌 location 用——Location 头本身保持原样) */
|
||||||
function send(res, status, contentType, body, isHead, extraHeaders = undefined) {
|
function escapeHtml(s) {
|
||||||
|
return s.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 头纪律唯一出口:全响应 noindex;text/html 附 CSP;扩展头(Location/Allow 等) */
|
||||||
|
function applyHeaders(res, status, contentType, extraHeaders = undefined) {
|
||||||
res.statusCode = status
|
res.statusCode = status
|
||||||
res.setHeader('X-Robots-Tag', X_ROBOTS_TAG)
|
res.setHeader('X-Robots-Tag', X_ROBOTS_TAG)
|
||||||
if (contentType.startsWith('text/html')) res.setHeader('Content-Security-Policy', CSP)
|
if (contentType.startsWith('text/html')) res.setHeader('Content-Security-Policy', CSP)
|
||||||
@@ -81,6 +90,11 @@ function send(res, status, contentType, body, isHead, extraHeaders = undefined)
|
|||||||
for (const [name, value] of Object.entries(extraHeaders)) res.setHeader(name, value)
|
for (const [name, value] of Object.entries(extraHeaders)) res.setHeader(name, value)
|
||||||
}
|
}
|
||||||
res.setHeader('Content-Type', contentType)
|
res.setHeader('Content-Type', contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 固定小体响应出口(404/410/405/redirect 页):HEAD 不写 body(Content-Length 仍按实体设置) */
|
||||||
|
function send(res, status, contentType, body, isHead, extraHeaders = undefined) {
|
||||||
|
applyHeaders(res, status, contentType, extraHeaders)
|
||||||
res.setHeader('Content-Length', String(Buffer.byteLength(body)))
|
res.setHeader('Content-Length', String(Buffer.byteLength(body)))
|
||||||
res.end(isHead ? undefined : body)
|
res.end(isHead ? undefined : body)
|
||||||
}
|
}
|
||||||
@@ -95,8 +109,9 @@ const GONE_HTML = htmlDocument('410 Gone', '此内容已被永久移除(route-
|
|||||||
* 创建预览服务器(不监听——start() 才绑定)。返回 {server, start, stop}:
|
* 创建预览服务器(不监听——start() 才绑定)。返回 {server, start, stop}:
|
||||||
* start() → Promise<AddressInfo>(port=0 时返回实际端口);stop() 关连接再关实例
|
* start() → Promise<AddressInfo>(port=0 时返回实际端口);stop() 关连接再关实例
|
||||||
* (closeAllConnections 防 keep-alive 空闲套接字吊住 close)。
|
* (closeAllConnections 防 keep-alive 空闲套接字吊住 close)。
|
||||||
|
* quiet: true 静默逐请求访问日志(缺省逐请求一行 stdout)。
|
||||||
*/
|
*/
|
||||||
export function createPreviewServer({ distDir, routeManifest, host = '127.0.0.1', port = 4173 }) {
|
export function createPreviewServer({ distDir, routeManifest, host = '127.0.0.1', port = 4173, quiet = false }) {
|
||||||
if (!LOOPBACK_HOSTS.has(host)) {
|
if (!LOOPBACK_HOSTS.has(host)) {
|
||||||
throw new Error(`预览服务器只允许回环绑定(127.0.0.1 / localhost),拒绝 host=${host}`)
|
throw new Error(`预览服务器只允许回环绑定(127.0.0.1 / localhost),拒绝 host=${host}`)
|
||||||
}
|
}
|
||||||
@@ -172,14 +187,27 @@ export function createPreviewServer({ distDir, routeManifest, host = '127.0.0.1'
|
|||||||
await sendNotFound(res, isHead)
|
await sendNotFound(res, isHead)
|
||||||
return 404
|
return 404
|
||||||
}
|
}
|
||||||
const body = await readFile(realAbs)
|
|
||||||
const contentType =
|
const contentType =
|
||||||
typeof row.content_type === 'string' && row.content_type !== ''
|
typeof row.content_type === 'string' && row.content_type !== ''
|
||||||
? row.content_type.startsWith('text/html')
|
? row.content_type.startsWith('text/html')
|
||||||
? 'text/html; charset=utf-8'
|
? 'text/html; charset=utf-8'
|
||||||
: row.content_type
|
: row.content_type
|
||||||
: contentTypeForExt(rel)
|
: contentTypeForExt(rel)
|
||||||
send(res, row.status || 200, contentType, body, isHead)
|
// 流式服务:Content-Length 取自先行 stat(body 不经内存驻留——大媒体文件 RSS 平坦);
|
||||||
|
// HEAD 只发头零读盘(Content-Length 仍按实体设置,符合语义)
|
||||||
|
applyHeaders(res, row.status || 200, contentType)
|
||||||
|
res.setHeader('Content-Length', String(st.size))
|
||||||
|
if (isHead) {
|
||||||
|
res.end()
|
||||||
|
return row.status || 200
|
||||||
|
}
|
||||||
|
createReadStream(realAbs)
|
||||||
|
.on('error', (err) => {
|
||||||
|
// 头已发出无法改码——掐断连接(客户端见截断传输);进程不因未处理 error 崩
|
||||||
|
console.error(`[preview] 文件流失败 ${rel}:`, err)
|
||||||
|
res.destroy()
|
||||||
|
})
|
||||||
|
.pipe(res)
|
||||||
return row.status || 200
|
return row.status || 200
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +243,9 @@ export function createPreviewServer({ distDir, routeManifest, host = '127.0.0.1'
|
|||||||
if (row.kind === 'redirect') {
|
if (row.kind === 'redirect') {
|
||||||
const location = typeof row.location === 'string' ? row.location : '/'
|
const location = typeof row.location === 'string' ? row.location : '/'
|
||||||
const status = row.status >= 300 && row.status <= 399 ? row.status : 301
|
const status = row.status >= 300 && row.status <= 399 ? row.status : 301
|
||||||
send(res, status, 'text/html; charset=utf-8', htmlDocument('Moved', `本页已迁移:<a href="${location}">${location}</a>`), isHead, {
|
// Location 头原样;HTML 迁移页内嵌时转义(</&/" ——防 manifest location 注入标记)
|
||||||
|
const safeLocation = escapeHtml(location)
|
||||||
|
send(res, status, 'text/html; charset=utf-8', htmlDocument('Moved', `本页已迁移:<a href="${safeLocation}">${safeLocation}</a>`), isHead, {
|
||||||
Location: location,
|
Location: location,
|
||||||
})
|
})
|
||||||
return status
|
return status
|
||||||
@@ -233,7 +263,9 @@ export function createPreviewServer({ distDir, routeManifest, host = '127.0.0.1'
|
|||||||
const startedAt = performance.now()
|
const startedAt = performance.now()
|
||||||
dispatch(req, res)
|
dispatch(req, res)
|
||||||
.then((status) => {
|
.then((status) => {
|
||||||
console.log(`[preview] ${req.method} ${req.url} ${status} ${(performance.now() - startedAt).toFixed(1)}ms`)
|
if (!quiet) {
|
||||||
|
console.log(`[preview] ${req.method} ${req.url} ${status} ${(performance.now() - startedAt).toFixed(1)}ms`)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
// 兜底:文件读取中途失败等未预期错误——500(若头未发出)
|
// 兜底:文件读取中途失败等未预期错误——500(若头未发出)
|
||||||
@@ -275,9 +307,16 @@ function parseCliArgs(argv) {
|
|||||||
for (let i = 0; i < argv.length; i++) {
|
for (let i = 0; i < argv.length; i++) {
|
||||||
const a = argv[i]
|
const a = argv[i]
|
||||||
if (a === '--dist' || a === '--manifest' || a === '--port' || a === '--host') {
|
if (a === '--dist' || a === '--manifest' || a === '--port' || a === '--host') {
|
||||||
out[a.slice(2)] = argv[++i]
|
const value = argv[i + 1]
|
||||||
|
if (value === undefined) {
|
||||||
|
throw new Error(`参数 ${a} 缺值(可用:--dist <dir> --manifest <path> [--port 4173] [--host 127.0.0.1] [--quiet])`)
|
||||||
|
}
|
||||||
|
out[a.slice(2)] = value
|
||||||
|
i++
|
||||||
|
} else if (a === '--quiet') {
|
||||||
|
out.quiet = true
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`未知参数:${a}(可用:--dist <dir> --manifest <path> [--port 4173] [--host 127.0.0.1])`)
|
throw new Error(`未知参数:${a}(可用:--dist <dir> --manifest <path> [--port 4173] [--host 127.0.0.1] [--quiet])`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
@@ -286,7 +325,7 @@ function parseCliArgs(argv) {
|
|||||||
async function main() {
|
async function main() {
|
||||||
const args = parseCliArgs(process.argv.slice(2))
|
const args = parseCliArgs(process.argv.slice(2))
|
||||||
if (typeof args.dist !== 'string' || typeof args.manifest !== 'string') {
|
if (typeof args.dist !== 'string' || typeof args.manifest !== 'string') {
|
||||||
throw new Error('用法:node server.mjs --dist <dir> --manifest <path> [--port 4173] [--host 127.0.0.1]')
|
throw new Error('用法:node server.mjs --dist <dir> --manifest <path> [--port 4173] [--host 127.0.0.1] [--quiet]')
|
||||||
}
|
}
|
||||||
const port = args.port === undefined ? 4173 : Number(args.port)
|
const port = args.port === undefined ? 4173 : Number(args.port)
|
||||||
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`--port 非法(0-65535 整数):${args.port}`)
|
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`--port 非法(0-65535 整数):${args.port}`)
|
||||||
@@ -296,6 +335,7 @@ async function main() {
|
|||||||
routeManifest: manifest,
|
routeManifest: manifest,
|
||||||
host: args.host ?? '127.0.0.1',
|
host: args.host ?? '127.0.0.1',
|
||||||
port,
|
port,
|
||||||
|
quiet: args.quiet === true,
|
||||||
})
|
})
|
||||||
const addr = await preview.start()
|
const addr = await preview.start()
|
||||||
console.log(`[preview] dist=${resolve(args.dist)} routes=${manifest.routes.length}`)
|
console.log(`[preview] dist=${resolve(args.dist)} routes=${manifest.routes.length}`)
|
||||||
|
|||||||
+9
-2
@@ -358,7 +358,8 @@ program
|
|||||||
.requiredOption('--build <jobId>', '构建 job id(须 succeeded——dist 产物只在成功终态存在)')
|
.requiredOption('--build <jobId>', '构建 job id(须 succeeded——dist 产物只在成功终态存在)')
|
||||||
.option('--port <port>', '监听端口(0 = 随机可用端口)', '4173')
|
.option('--port <port>', '监听端口(0 = 随机可用端口)', '4173')
|
||||||
.option('--host <host>', '绑定地址——仅允许 127.0.0.1 / localhost(回环纪律,外值拒绝)', '127.0.0.1')
|
.option('--host <host>', '绑定地址——仅允许 127.0.0.1 / localhost(回环纪律,外值拒绝)', '127.0.0.1')
|
||||||
.action(async (opts: { build: string; port: string; host: string }) => {
|
.option('--quiet', '静默逐请求访问日志(缺省逐请求一行 stdout)')
|
||||||
|
.action(async (opts: { build: string; port: string; host: string; quiet?: boolean }) => {
|
||||||
// 回环校验先于一切(含 DB 访问)——预览永不对外,外值在触碰任何资源前拒绝
|
// 回环校验先于一切(含 DB 访问)——预览永不对外,外值在触碰任何资源前拒绝
|
||||||
if (opts.host !== '127.0.0.1' && opts.host !== 'localhost') {
|
if (opts.host !== '127.0.0.1' && opts.host !== 'localhost') {
|
||||||
throw new Error(`--host 仅允许 127.0.0.1 / localhost(回环绑定纪律,外值拒绝):${opts.host}`)
|
throw new Error(`--host 仅允许 127.0.0.1 / localhost(回环绑定纪律,外值拒绝):${opts.host}`)
|
||||||
@@ -404,7 +405,13 @@ program
|
|||||||
}
|
}
|
||||||
// server.mjs 是纯 JS 包(零依赖、不进 TS 链)——动态导入直连源文件,类型面由 server.d.mts 供给
|
// server.mjs 是纯 JS 包(零依赖、不进 TS 链)——动态导入直连源文件,类型面由 server.d.mts 供给
|
||||||
const { createPreviewServer } = await import('../apps/preview-server/server.mjs')
|
const { createPreviewServer } = await import('../apps/preview-server/server.mjs')
|
||||||
const preview = createPreviewServer({ distDir: distPath, routeManifest: manifestRaw, host: opts.host, port })
|
const preview = createPreviewServer({
|
||||||
|
distDir: distPath,
|
||||||
|
routeManifest: manifestRaw,
|
||||||
|
host: opts.host,
|
||||||
|
port,
|
||||||
|
quiet: opts.quiet === true,
|
||||||
|
})
|
||||||
const addr = await preview.start()
|
const addr = await preview.start()
|
||||||
console.log(`[preview:start] job=${opts.build}`)
|
console.log(`[preview:start] job=${opts.build}`)
|
||||||
console.log(` dist: ${distPath}`)
|
console.log(` dist: ${distPath}`)
|
||||||
|
|||||||
@@ -15,7 +15,9 @@
|
|||||||
// 9. 目录列表禁用(dist 子目录请求 → 404,不出文件清单)
|
// 9. 目录列表禁用(dist 子目录请求 → 404,不出文件清单)
|
||||||
// 10. HEAD 方法支持(200 同头无 body)
|
// 10. HEAD 方法支持(200 同头无 body)
|
||||||
// 11. CLI:--host 外值拒绝(校验先于 DB 访问——tsx 直跑 sf.ts)
|
// 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 { execFile, spawn } from 'node:child_process'
|
||||||
import { createHash } from 'node:crypto'
|
import { createHash } from 'node:crypto'
|
||||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
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 { tmpdir } from 'node:os'
|
||||||
import { dirname, join, resolve } from 'node:path'
|
import { dirname, join, resolve } from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
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 type { RouteManifest, RouteManifestRoute } from '@sf/contracts'
|
||||||
import { createBuildJob } from '../../apps/api/src/modules/build/build.service.js'
|
import { createBuildJob } from '../../apps/api/src/modules/build/build.service.js'
|
||||||
import { createImportJob } from '../../apps/api/src/modules/import/import.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 jobId = manifest!.build_job_id
|
||||||
const child = spawn(
|
const child = spawn(
|
||||||
process.execPath,
|
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'] },
|
{ cwd: REPO_ROOT, stdio: ['ignore', 'pipe', 'pipe'] },
|
||||||
)
|
)
|
||||||
let stdout = ''
|
let stdout = ''
|
||||||
@@ -379,6 +381,9 @@ describe('预览服务器(真实构建产物 + 手构 manifest 扩展)', ()
|
|||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
expect(res.headers.get('x-robots-tag')).toBe('noindex, nofollow')
|
expect(res.headers.get('x-robots-tag')).toBe('noindex, nofollow')
|
||||||
expect(await res.text()).toStrictEqual(await readFile(join(distDir, 'index.html'), 'utf8'))
|
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 {
|
} finally {
|
||||||
child.kill()
|
child.kill()
|
||||||
await new Promise<void>((resolveP) => {
|
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()
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user