Skip to content

Commit 57783bf

Browse files
Improve test performance & add test benches (#329)
1 parent 722552a commit 57783bf

21 files changed

Lines changed: 1004 additions & 109 deletions

path/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Thin entries `path/fount.{ps1,sh}` dispatch to `path/src/cmd/<name>.*` via inlin
1616

1717
`fount init` (and first install) registers a non-Steam shortcut when Steam is present — skip otherwise. Windows launchers use `$FOUNT_DIR/fount.exe` (written if missing, gitignored); other platforms use `path/fount`. `fount geneexe [path]` still defaults to `./fount.exe` (cwd). `remove` unregisters this install only (level 85, before Deno uninstall). No `data/config.json` (except `remove`) is `ensure_fount_config` / `Ensure-FountConfig` then the original command — not `cmd_open`. Interactive first-run starts `:8930` (wait/install liveness) and opens `wait/install/?from=runner`; `FOUNT_INSTALL_WAIT=1` is exported and `:8930` stays up until that process exits (after the dispatched command). `cmd_open` opens `wait?cold_bootting=true` only when that flag is unset. Docker / `FOUNT_ACCEPT_EULA` skip the prompt and copy default config; refusing the EULA or running without a console removes the installation (`N` → `fount remove`). See [runner AGENTS](../src/runner/AGENTS.md). `fount.exe` compile / Steam registration traps (ps12exe, favicon, `shortcuts.vdf`): [docs/exe-notes.md](docs/exe-notes.md).
1818

19-
Same logic is isomorphic across `foo.{ps1,sh}`; platform-only code under `path/src/win/` or `unix/`. Shared helpers: `in_container`, `run_with_updates`, `trap_terminal_teardown` (optional extra cleanup function name), `handle_docker_passthrough`, `check_temp_guard`, `sed_escape`.
19+
Same logic is isomorphic across `foo.{ps1,sh}`; platform-only code under `path/src/win/` or `unix/`. Shared helpers: `in_container`, `run_with_updates`, `trap_terminal_teardown` (optional extra cleanup function name), `handle_docker_passthrough`, `check_temp_guard`, `sed_escape`. Server liveness: pwsh uses `Test-FountRunning` (from `fount-pwsh`, IPC ping 16698, ~100ms fast-fail); sh uses `test_fount_running` (`unix/ipc`, same IPC ping via nc/socat). `cmd_default` (bare `fount`) uses it to skip `background keepalive` and go straight to `log` when the server is already listening; on probe failure (no module/nc) it falls back to starting keepalive. Deno-side port probe (test kernel): `src/scripts/listener.mjs`.
2020

2121
`remove` scans `**/*.uninstall.<level>.*` under `path/src`, highest level first; level `0` deletes the install tree.
2222

path/src/cmd/default.ps1

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@
1010
& (Join-Path $FOUNT_DIR 'path/fount.ps1') keepalive @args
1111
}
1212
else {
13-
Write-TaskbarProgress -Percent 25
14-
Set-Title "𝓯"
15-
& (Join-Path $FOUNT_DIR 'path/fount.ps1') background keepalive @args
16-
Set-Title "𝓯𝓸"
17-
Write-TaskbarProgress
13+
# 服务器已在运行则只启 log viewer,不再重复拉一个 keepalive(省一次无效服务器启动)。
14+
# Test-FountRunning 来自 fount-pwsh 模块(IPC ping 16698,~100ms 快速失败);模块缺失时按未运行回退。
15+
if (-not (try { Import-Module fount-pwsh -ErrorAction Stop; Test-FountRunning } catch { $false })) {
16+
Write-TaskbarProgress -Percent 25
17+
Set-Title "𝓯"
18+
& (Join-Path $FOUNT_DIR 'path/fount.ps1') background keepalive @args
19+
Set-Title "𝓯𝓸"
20+
Write-TaskbarProgress
21+
}
1822
& (Join-Path $FOUNT_DIR 'path/fount.ps1') log
1923
}
2024
exit $LastExitCode

path/src/cmd/default.sh

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ cmd_default() {
99
"$0" keepalive "$@"
1010
exit $?
1111
fi
12-
write_taskbar_progress 25
13-
set_title "𝓯"
14-
"$0" background keepalive "$@"
15-
set_title "𝓯𝓸"
16-
write_taskbar_progress
12+
# 服务器已在运行则只启 log viewer,不再重复拉一个 keepalive(省一次无效服务器启动)。
13+
require unix/ipc
14+
if ! test_fount_running; then
15+
write_taskbar_progress 25
16+
set_title "𝓯"
17+
"$0" background keepalive "$@"
18+
set_title "𝓯𝓸"
19+
write_taskbar_progress
20+
fi
1721
"$0" log
1822
exit $?
1923
}

path/src/deno.ps1

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,12 @@ function script:install_deno {
9999
if (!(Get-Command deno -ErrorAction SilentlyContinue)) {
100100
Write-Host (Get-I18n -key 'deno.installFailedFallback')
101101
$url = "https://github.com/denoland/deno/releases/latest/download/deno-" + $(if ($IsWindows) {
102-
"x86_64-pc-windows-msvc.zip"
102+
if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) {
103+
"aarch64-pc-windows-msvc.zip"
104+
}
105+
else {
106+
"x86_64-pc-windows-msvc.zip"
107+
}
103108
}
104109
elseif ($IsMacOS) {
105110
if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) {

path/src/unix/ipc.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@ install_ipc_tools() {
44
install_package "nc" "netcat gnu-netcat openbsd-netcat netcat-openbsd nmap-ncat" || install_package "socat" "socat"
55
}
66

7+
# fount 服务器是否在运行:IPC ping(16698,newline 结尾 JSON),等价 pwsh 的 Test-FountRunning。
8+
# 需要 nc 或 socat;都没有时返回 1(调用方按“未运行”处理,回落到启动服务器)。
9+
test_fount_running() {
10+
local cmd_json='{"type":"ping","data":{}}' response=""
11+
if command -v nc >/dev/null 2>&1; then
12+
response=$(printf '%s\n' "$cmd_json" | nc -w 1 localhost 16698 2>/dev/null)
13+
elif command -v socat >/dev/null 2>&1; then
14+
response=$(printf '%s\n' "$cmd_json" | socat -T 2 - TCP:localhost:16698,nodelay 2>/dev/null)
15+
else
16+
return 1
17+
fi
18+
[ -n "$response" ] && printf '%s' "$response" | grep -q '"status"[[:space:]]*:[[:space:]]*"ok"'
19+
}
20+
721
# Expects TARGET_URL to be exported by the caller.
822
read -r -d '' BACKGROUND_IPC_JOB <<'BGJOB'
923
ipc_call() {

path/test/runtime_update.test.mjs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,7 @@ const pwshAvailable = (await available).pwsh || (await available).powershell
5959
function pwshTest(name, test) {
6060
Deno.test({
6161
name,
62-
/**
63-
*
64-
*/
62+
/** 测试主体:构造临时状态目录并在 pwsh 中执行用例。 */
6563
fn: async () => {
6664
const root = mkdtempSync(join(tmpdir(), 'fount-pkg-pwsh-'))
6765
const stateDir = join(root, 'state')
@@ -107,10 +105,10 @@ function tempEnv() {
107105
for (const [name, setup, expected] of [
108106
['dpkg/apt', 'dpkg() { printf \'mypkg: %s\\n\' "$2"; }', 'apt-get mypkg'],
109107
['pacman', 'pacman() { printf \'mypkg\\n\'; }', 'pacman mypkg'],
110-
['rpm+dnf', 'rpm() { printf \'mypkg\\n\'; } dnf() { :; }', 'dnf mypkg'],
111-
['rpm+yum', 'rpm() { printf \'mypkg\\n\'; } yum() { :; }', 'yum mypkg'],
112-
['rpm+zypper', 'rpm() { printf \'mypkg\\n\'; } zypper() { :; }', 'zypper mypkg'],
113-
['apk', 'apk() { printf \'is owned by mypkg\\n\'; }', 'apk mypkg'],
108+
['rpm+dnf', 'rpm() { printf \'mypkg\\n\'; }; dnf() { :; }', 'dnf mypkg'],
109+
['rpm+yum', 'rpm() { printf \'mypkg\\n\'; }; yum() { :; }', 'yum mypkg'],
110+
['rpm+zypper', 'rpm() { printf \'mypkg\\n\'; }; zypper() { :; }', 'zypper mypkg'],
111+
['apk', 'apk() { printf \'/usr/bin/demo is owned by mypkg\\n\'; }', 'apk mypkg'],
114112
['brew', 'brew() { printf \'/usr/local\\n\'; }', 'brew deno'],
115113
['pkg', 'pkg() { printf \'mypkg\\n\'; }', 'pkg mypkg'],
116114
['none', '', ''],
@@ -419,7 +417,7 @@ bashTest('the standalone update-deno script upgrades an unmanaged Deno end-to-en
419417
mkdirSync(stubBin, { recursive: true })
420418
Deno.symlinkSync(join(REPO_ROOT, 'path'), join(fountDir, 'path'), 'dir')
421419
Deno.symlinkSync(join(REPO_ROOT, 'src/public/locales'), join(fountDir, 'src/public/locales'), 'dir')
422-
writeFileSync(join(stubBin, 'deno'), `#!/bin/sh\nif [ "$1" = "-V" ]; then echo "deno 2.9.5"; elif [ "$1" = "upgrade" ]; then echo "deno:upgrade $*" >>"${'$'}FOUNT_DENO_TRACE"; fi\n`)
420+
writeFileSync(join(stubBin, 'deno'), `#!/bin/sh\nif [ "$1" = "-V" ]; then echo "deno 2.9.5"; elif [ "$1" = "upgrade" ]; then echo "deno:$*" >>"${'$'}FOUNT_DENO_TRACE"; fi\n`)
423421
writeFileSync(join(stubBin, 'jq'), '#!/bin/sh\necho {}\n')
424422
Deno.chmodSync(join(stubBin, 'deno'), 0o755)
425423
Deno.chmodSync(join(stubBin, 'jq'), 0o755)

src/public/parts/shells/chat/test/frontend/markdownRichInput.spec.mjs

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,6 @@ import {
55

66
const ENTITY_HASH = 'f'.repeat(128)
77

8-
/**
9-
* 自定义文件 token 解析(code shell 场景)。
10-
* @param {string} raw 原始 token
11-
* @returns {{ kind: 'file', body: string, name: string }} token 描述
12-
*/
13-
function parseFileToken(raw) {
14-
return { kind: 'file', body: raw, name: raw.slice(6, -1) }
15-
}
16-
17-
/**
18-
* 自定义文件 token chip 标签。
19-
* @param {{ name: string }} token token 描述
20-
* @returns {string} 标签
21-
*/
22-
function fileTokenLabel(token) {
23-
return token.name
24-
}
25-
268
test.describe('Markdown rich input', () => {
279
test('clicking empty composer places caret at start (before placeholder)', async ({ page, groupChannel: _ }) => {
2810
const input = page.locator('#message-input')
@@ -80,9 +62,22 @@ test.describe('Markdown rich input', () => {
8062
await expect(input.locator('.fount-markdown-rich-input-chip.fount-markdown-rich-input-emoji')).toHaveCount(1)
8163
})
8264

83-
test('inlineTokens option renders custom token chip without registered defaults', async ({ page }) => {
65+
test('inlineTokens option renders custom token chip without registered defaults', async ({ page, baseUrl }) => {
66+
await page.goto(`${baseUrl}/parts/shells:chat/hub/`, { waitUntil: 'domcontentloaded' })
8467
const result = await page.evaluate(async () => {
8568
const { createMarkdownRichInput } = await import('/scripts/components/markdownRichInput.mjs')
69+
/**
70+
* 自定义文件 token 解析(code shell 场景)。
71+
* @param {string} raw 原始 token
72+
* @returns {{ kind: 'file', body: string, name: string }} token 描述
73+
*/
74+
const parseFileToken = raw => ({ kind: 'file', body: raw, name: raw.slice(6, -1) })
75+
/**
76+
* 自定义文件 token chip 标签。
77+
* @param {{ name: string }} token token 描述
78+
* @returns {string} 标签
79+
*/
80+
const fileTokenLabel = token => token.name
8681
const el = document.createElement('div')
8782
document.body.appendChild(el)
8883
const handle = createMarkdownRichInput(el, {
@@ -107,7 +102,8 @@ test.describe('Markdown rich input', () => {
107102
expect(result).toEqual({ fileChipCount: 1, roundTrip: true, mentionChipCount: 0 })
108103
})
109104

110-
test('useRegisteredInlineTokens=false keeps registered mention token as plain text', async ({ page }) => {
105+
test('useRegisteredInlineTokens=false keeps registered mention token as plain text', async ({ page, baseUrl }) => {
106+
await page.goto(`${baseUrl}/parts/shells:chat/hub/`, { waitUntil: 'domcontentloaded' })
111107
const result = await page.evaluate(async () => {
112108
const { createMarkdownRichInput } = await import('/scripts/components/markdownRichInput.mjs')
113109
const el = document.createElement('div')

src/scripts/listener.mjs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* 本地端口监听探查:查谁在听某个端口,或该端口是否在监听。
3+
*
4+
* 背景:Windows 上对刚关闭端口的 fetch 会挂满健康检查超时才返回(SYN 重试),
5+
* 因此「本地服务在不在」这类探活若只靠 HTTP/WS 轮询会白白等待。改用 netstat/lsof
6+
* 直接读 TCP 表,能在 ~100ms 内判定端口是否被监听,避免长时间轮询。
7+
*
8+
* 使用(仅 Deno/Node 环境;pwsh/sh 无法 import,见 path 侧 native 实现):
9+
* import { isPortListening } from '../scripts/listener.mjs'
10+
*/
11+
import { execFile as execFileCallback } from 'node:child_process'
12+
import process from 'node:process'
13+
import { promisify } from 'node:util'
14+
15+
const execFile = promisify(execFileCallback)
16+
17+
/**
18+
* 从 netstat -ano 行里抠 LISTENING pid。
19+
* @param {string} stdout netstat 输出
20+
* @param {number} port 端口
21+
* @returns {number} pid;没有则为 0
22+
*/
23+
export function parseNetstatListenPid(stdout, port) {
24+
const token = `:${port}`
25+
for (const line of stdout.split(/\r?\n/)) {
26+
if (!/listen/i.test(line) && !line.includes('侦听')) continue
27+
const index = line.indexOf(token)
28+
if (index < 0) continue
29+
const after = line[index + token.length]
30+
if (after && ![' ', '\t'].includes(after)) continue
31+
const pid = Number(line.trim().split(/\s+/).at(-1))
32+
if (pid > 0) return pid
33+
}
34+
return 0
35+
}
36+
37+
/**
38+
* 查谁在听这个端口;没有则为 0。
39+
* @param {number} port 端口
40+
* @returns {Promise<number | null>} 监听进程 pid;无人监听为 0,探查出错为 null
41+
*/
42+
export async function listenerPid(port) {
43+
try {
44+
if (process.platform === 'win32')
45+
return parseNetstatListenPid(String((await execFile('netstat', ['-ano', '-p', 'tcp'], { windowsHide: true })).stdout), port)
46+
// macOS 自带 lsof 4.90 不支持 -Q;其余平台补上 -Q,让「无匹配监听」返回 0 而非
47+
// 退出码 1(否则空结果会误走下方 catch 返回 null),真实执行失败仍走 catch。
48+
const args = ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t']
49+
if (process.platform !== 'darwin') args.push('-Q')
50+
const pid = Number(String((await execFile('lsof', args)).stdout).trim().split(/\s+/)[0])
51+
return pid > 0 ? pid : 0
52+
}
53+
catch {
54+
return null
55+
}
56+
}
57+
58+
/**
59+
* 端口当前是否有进程在监听。
60+
* @param {number} port 端口
61+
* @returns {Promise<boolean | null>} 是否在监听;探查出错(无法判定)为 null
62+
*/
63+
export async function isPortListening(port) {
64+
const pid = await listenerPid(port)
65+
if (pid === null) return null
66+
return pid !== 0
67+
}

src/scripts/test/AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ Manifest id = domain (`server`, `testkit`, `p2p`, `shells/chat`, …).
8383

8484
## Operator tools
8585

86+
- **Performance bench (`tools/bench/`)**: four standalone scripts that break down `fount test` wall time — `kernel_startup.mjs` (spawn→healthy with in-kernel phase split: init/link, graph eval, catalog load, bind), `kernel_link.mjs` (Deno init+link vs graph eval vs kernel logic, plus `deno cache` cost), `test_cycle.mjs` (per-suite child spawn/read/rm overhead via the real `buildSuiteInvocation`/`runCommand`), `viewer_cycle.mjs` (WS connect→hello→accepted→close round trips). Kernel records env-gated phases to `FOUNT_TEST_BENCH_PHASES_FILE` (`onPhase` callback through `startTestKernel`/`TestKernel.start`); tools spawn on `TEST_PORT_BASE+20000` and shut down each iteration. Run from repo root: `deno run --allow-scripts --allow-all -c ./deno.json ./src/scripts/test/tools/bench/<tool>.mjs [iterations]`. Known hot spots (baseline on this machine): kernel `deno cache` cold ≈17s (amortized across runs), kernel spawn→healthy ≈1s (dominated by Deno graph eval, ~500ms), per-suite `deno test --no-check` child spawn ≈120ms (Deno-runtime-bound). Optimizations applied: port liveness probes live in shared `src/scripts/listener.mjs` (`listenerPid`/`isPortListening`/`parseNetstatListenPid`); kernel shutdown + `kernelHealthy` check the port first (netstat/lsof, ~50–100ms) instead of waiting the 1.5s health-check timeout on a closed Windows port (`ensure.mjs`); manifest discovery walks directories with bounded concurrency (`findManifestFilesParallel`, `manifest.mjs`), cutting `catalogLoad` from ~220ms to ~80ms and `loadAllSuites` from ~170ms to ~50ms.
8687
- **Hung run**: `data/test/state/logs/`; rerun with env from the log. Watchdogs / sleep retry / baselines: [host-keep-awake.md](docs/host-keep-awake.md), [resource-scheduling.md](docs/resource-scheduling.md). Opt out: `FOUNT_TEST_ALLOW_SLEEP=1`. Module-check mutex leaks (killed Deno child never POSTs ready) auto-release the mutex after the idle window and still fail the suite as missed-ready; they must not freeze later suites. Stuck detached kernel: `fount test --kernel shutdown` / `--kernel reboot`.
8788
- **Deno panic auto-report**: `core/deno_panic.mjs``denoland/deno` (if `gh` + auth); dedup `data/test/deno_panics.json`. Override: `FOUNT_DENO_PANIC_REPO`. `testkit` excluded.
8889
- **`[aria-ignore]`**: value = GitHub issue URL; closed-state via hub `github_issue` + Playwright `assertAriaIgnoreIssues`. Policy: `pages/scripts/test/aria_ignore.mjs`. No hub / `gh` → treat as still open. Page watch: [playwright.md](docs/playwright.md#page-watch).

src/scripts/test/core/manifest.mjs

Lines changed: 90 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -142,34 +142,102 @@ const SKIP_DIR_NAMES = new Set([
142142
'dist',
143143
])
144144

145+
/** 并行目录遍历并发上限(readdir 大量并发对 Windows 无益,8 足够吃满磁盘队列)。 */
146+
const MANIFEST_WALK_CONCURRENCY = 8
147+
145148
/**
146-
* 递归查找 test/manifest.json。
147-
* @param {string} dir 当前目录绝对路径
148-
* @returns {AsyncGenerator<string>} manifest 绝对路径
149+
* 有界并发的递归 readdir 扫描:并行兄弟目录,避免 700+ 目录逐个串行遍历。
150+
* 槽位只在单次 readdir 期间持有(不跨子目录等待),深目录链不会占满并发导致死锁。
151+
* @param {string} directory 起始目录绝对路径
152+
* @returns {Promise<string[]>} 找到的 manifest 绝对路径(按路径排序)
149153
*/
150-
async function* findManifestFiles(dir) {
151-
let entries
152-
try {
153-
entries = await readdir(dir, { withFileTypes: true })
154-
}
155-
catch {
156-
return
154+
async function findManifestFilesParallel(directory) {
155+
/** @type {string[]} */
156+
const found = []
157+
let active = 0
158+
/** @type {(() => void)[]} */
159+
const waiters = []
160+
/** 未完成的目录/stat 任务数;归零即全部完成。 */
161+
let pending = 1
162+
let drainedResolve
163+
/** @type {Promise<void>} */
164+
const drained = new Promise(resolve => { drainedResolve = resolve })
165+
166+
/**
167+
* 获取一个 readdir 槽位;超出并发时排队。
168+
* @returns {Promise<void>} 槽位可用
169+
*/
170+
const acquire = () => new Promise(resolve => {
171+
if (active < MANIFEST_WALK_CONCURRENCY) {
172+
active++
173+
resolve()
174+
}
175+
else waiters.push(resolve)
176+
})
177+
/**
178+
* 释放一个槽位;有排队等待者则把槽位直接转交给最先等待者,
179+
* 避免被唤醒者(自身未 acquire)再递减 active 导致并发计数失真。
180+
* @returns {void}
181+
*/
182+
const release = () => {
183+
if (waiters.length) waiters.shift()()
184+
else active--
157185
}
158-
for (const entry of entries) {
159-
if (!entry.isDirectory()) continue
160-
if (SKIP_DIR_NAMES.has(entry.name)) continue
161-
const path = join(dir, entry.name)
162-
if (entry.name === 'test') {
163-
const manifestPath = join(path, 'manifest.json')
164-
try {
165-
await stat(manifestPath)
166-
yield manifestPath
186+
187+
/**
188+
* 遍历单个目录:readdir 结束后立即释放槽位,再派发子目录。
189+
* @param {string} path 目录绝对路径
190+
* @returns {Promise<void>} 完成
191+
*/
192+
const processDirectory = async path => {
193+
await acquire()
194+
let entries
195+
try {
196+
entries = await readdir(path, { withFileTypes: true })
197+
}
198+
catch {
199+
release()
200+
return
201+
}
202+
release()
203+
for (const entry of entries) {
204+
if (!entry.isDirectory()) continue
205+
if (SKIP_DIR_NAMES.has(entry.name)) continue
206+
const child = join(path, entry.name)
207+
if (entry.name === 'test') {
208+
const manifestPath = join(child, 'manifest.json')
209+
pending++
210+
stat(manifestPath).then(() => found.push(manifestPath)).catch(() => { }).finally(() => {
211+
pending--
212+
if (pending === 0) drainedResolve()
213+
})
214+
continue
167215
}
168-
catch { /* no manifest */ }
169-
continue
216+
pending++
217+
void processDirectory(child).finally(() => {
218+
pending--
219+
if (pending === 0) drainedResolve()
220+
})
170221
}
171-
yield* findManifestFiles(path)
172222
}
223+
224+
// 根目录本身也算一个 pending:派发完成后由其 finally 递减,全部子任务归零时 drained 落地。
225+
void processDirectory(directory).finally(() => {
226+
pending--
227+
if (pending === 0) drainedResolve()
228+
})
229+
await drained
230+
return found.sort()
231+
}
232+
233+
/**
234+
* 递归查找 test/manifest.json(并行遍历,顺序不保证)。
235+
* @param {string} dir 当前目录绝对路径
236+
* @returns {AsyncGenerator<string>} manifest 绝对路径
237+
*/
238+
async function* findManifestFiles(dir) {
239+
for (const manifestPath of await findManifestFilesParallel(dir))
240+
yield manifestPath
173241
}
174242

175243
/**

0 commit comments

Comments
 (0)