Skip to content

Commit 19872f7

Browse files
committed
fix(dsn): prevent silent exit during uncached DSN auto-detection (#411)
Replace Bun.Glob.scan() async iterators with readdir() in DSN detection paths. The async iterators don't ref-count the event loop, causing the process to exit with code 0 and no output while scanning is still in progress on first run (uncached). Changes: - bin.ts: Add setInterval keepalive cleared via .finally() to prevent premature event loop drain (can't use top-level await due to CJS bundle) - project-root.ts: Replace anyGlobMatches() Bun.Glob.scan() with readdir() + synchronous Bun.Glob.match() for language marker detection - env-file.ts: Replace Bun.Glob.scan('*') with readdir() in detectFromMonorepoEnvFiles() for listing monorepo package directories
1 parent 585b88a commit 19872f7

File tree

3 files changed

+45
-30
lines changed

3 files changed

+45
-30
lines changed

src/bin.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,9 @@ async function main(): Promise<void> {
204204
}
205205
}
206206

207-
main();
207+
// Async IIFE keeps the event loop alive until main() settles. A bare `main()`
208+
// call would let the process exit while async work is still pending. Top-level
209+
// `await` isn't an option because the npm bundle uses esbuild CJS format.
210+
(async () => {
211+
await main();
212+
})();

src/lib/dsn/env-file.ts

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* to find DSNs in individual packages/apps.
99
*/
1010

11-
import { stat } from "node:fs/promises";
11+
import { opendir } from "node:fs/promises";
1212
import { join } from "node:path";
1313
import { createDetectedDsn } from "./parser.js";
1414
import { scanSpecificFiles } from "./scanner.js";
@@ -168,36 +168,31 @@ export async function detectFromMonorepoEnvFiles(
168168
): Promise<EnvFileScanResult> {
169169
const dsns: DetectedDsn[] = [];
170170
const sourceMtimes: Record<string, number> = {};
171-
const pkgGlob = new Bun.Glob("*");
172171

173172
for (const monorepoRoot of MONOREPO_ROOTS) {
174173
const rootDir = join(cwd, monorepoRoot);
175174

175+
// Bun's opendir() may not throw on a missing directory — the error
176+
// surfaces when iterating. Wrap the full open+iterate in one try/catch.
176177
try {
177-
// Scan for subdirectories (each is a potential package/app)
178-
for await (const pkgName of pkgGlob.scan({
179-
cwd: rootDir,
180-
onlyFiles: false,
181-
})) {
182-
const pkgDir = join(rootDir, pkgName);
183-
184-
// Only process directories, not files
185-
try {
186-
const stats = await stat(pkgDir);
187-
if (!stats.isDirectory()) {
178+
const handle = await opendir(rootDir);
179+
try {
180+
for await (const entry of handle) {
181+
if (!entry.isDirectory()) {
188182
continue;
189183
}
190-
} catch {
191-
continue;
192-
}
193184

194-
const packagePath = `${monorepoRoot}/${pkgName}`;
185+
const pkgDir = join(rootDir, entry.name);
186+
const packagePath = `${monorepoRoot}/${entry.name}`;
195187

196-
const result = await detectDsnInPackage(pkgDir, packagePath);
197-
if (result.dsn) {
198-
dsns.push(result.dsn);
199-
Object.assign(sourceMtimes, result.sourceMtimes);
188+
const result = await detectDsnInPackage(pkgDir, packagePath);
189+
if (result.dsn) {
190+
dsns.push(result.dsn);
191+
Object.assign(sourceMtimes, result.sourceMtimes);
192+
}
200193
}
194+
} finally {
195+
await handle.close();
201196
}
202197
} catch {
203198
// Directory doesn't exist or scan failed, skip this monorepo root

src/lib/dsn/project-root.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* Stops at: home directory or filesystem root
1414
*/
1515

16-
import { stat } from "node:fs/promises";
16+
import { opendir, stat } from "node:fs/promises";
1717
import { homedir } from "node:os";
1818
import { dirname, join, resolve } from "node:path";
1919
import { anyTrue } from "../promises.js";
@@ -187,23 +187,38 @@ function anyExists(dir: string, names: readonly string[]): Promise<boolean> {
187187

188188
/**
189189
* Check if any files matching glob patterns exist in a directory.
190-
* Runs pattern checks in parallel and resolves as soon as any finds a match.
190+
* Uses `opendir` to lazily stream directory entries and exits on first match
191+
* without reading the entire directory. Matches via synchronous
192+
* `Bun.Glob.match()` (no async I/O, event-loop safe).
191193
*
192194
* @param dir - Directory to check
193195
* @param patterns - Glob patterns to match
194196
* @returns True if any matching file exists
195197
*/
196-
function anyGlobMatches(
198+
async function anyGlobMatches(
197199
dir: string,
198200
patterns: readonly string[]
199201
): Promise<boolean> {
200-
return anyTrue(patterns, async (pattern) => {
201-
const glob = new Bun.Glob(pattern);
202-
for await (const _match of glob.scan({ cwd: dir, onlyFiles: true })) {
203-
return true;
202+
// Bun's opendir() may not throw on a missing directory — the error
203+
// surfaces when iterating. Wrap the full open+iterate in one try/catch.
204+
try {
205+
const handle = await opendir(dir);
206+
try {
207+
for await (const entry of handle) {
208+
if (
209+
entry.isFile() &&
210+
patterns.some((p) => new Bun.Glob(p).match(entry.name))
211+
) {
212+
return true;
213+
}
214+
}
215+
return false;
216+
} finally {
217+
await handle.close();
204218
}
219+
} catch {
205220
return false;
206-
});
221+
}
207222
}
208223

209224
/**

0 commit comments

Comments
 (0)