Merge pull request #179 from sandypoli-boop/fix/windows-bun-mkdir-eexist

fix(baoyu-image-gen): tolerate Bun-on-Windows EEXIST from mkdir(recursive)
This commit is contained in:
Jim Liu 宝玉
2026-06-12 19:45:52 -05:00
committed by GitHub
2 changed files with 38 additions and 3 deletions
@@ -8,6 +8,7 @@ import type { CliArgs, ExtendConfig } from "./types.ts";
import {
createTaskArgs,
detectProvider,
ensureDir,
getConfiguredMaxWorkers,
getConfiguredProviderRateLimits,
getWorkerCount,
@@ -201,6 +202,26 @@ batch:
});
});
test("ensureDir creates nested dirs, is idempotent on an existing dir, and rethrows for a non-directory", async (t: TestContext) => {
const root = await makeTempDir("ensure-dir-");
t.after(() => fs.rm(root, { recursive: true, force: true }));
const nested = path.join(root, "a", "b", "c");
await ensureDir(nested);
assert.equal((await fs.stat(nested)).isDirectory(), true);
// Idempotent: a second call on an existing directory must not throw. This is the
// Bun-on-Windows regression the helper guards against (Bun wrongly throws EEXIST
// for mkdir(existingDir, { recursive: true })).
await ensureDir(nested);
// Rethrows when the path exists but is a file rather than a directory, so a real
// EEXIST against a non-directory is not silently swallowed.
const filePath = path.join(root, "not-a-dir");
await fs.writeFile(filePath, "x");
await assert.rejects(() => ensureDir(filePath));
});
test("loadExtendConfig renames legacy EXTEND.md when the new path is missing", async () => {
const root = await makeTempDir("baoyu-image-gen-extend-");
const cwd = path.join(root, "project");
+17 -3
View File
@@ -2,7 +2,7 @@ import path from "node:path";
import process from "node:process";
import { homedir } from "node:os";
import { fileURLToPath } from "node:url";
import { access, mkdir, readFile, rename, writeFile } from "node:fs/promises";
import { access, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
import type {
BatchFile,
BatchTaskInput,
@@ -563,11 +563,25 @@ async function exists(filePath: string): Promise<boolean> {
}
}
export async function ensureDir(dir: string): Promise<void> {
try {
await mkdir(dir, { recursive: true });
} catch (err) {
// Bun on Windows incorrectly throws EEXIST for mkdir(dir, { recursive: true })
// when the directory already exists, contradicting Node's documented contract
// (mkdir with recursive: true resolves silently for an existing directory).
// Tolerate EEXIST only when the path really is a directory; rethrow otherwise
// (e.g. EEXIST raised because the path points at an existing file).
if ((err as { code?: string }).code !== "EEXIST") throw err;
if (!(await stat(dir)).isDirectory()) throw err;
}
}
async function migrateLegacyExtendConfig(cwd: string, home: string): Promise<void> {
for (const { current, legacy } of getExtendConfigPathPairs(cwd, home)) {
const [hasCurrent, hasLegacy] = await Promise.all([exists(current), exists(legacy)]);
if (hasCurrent || !hasLegacy) continue;
await mkdir(path.dirname(current), { recursive: true });
await ensureDir(path.dirname(current));
await rename(legacy, current);
}
}
@@ -1049,7 +1063,7 @@ async function prepareBatchTasks(
}
async function writeImage(outputPath: string, imageData: Uint8Array): Promise<void> {
await mkdir(path.dirname(outputPath), { recursive: true });
await ensureDir(path.dirname(outputPath));
await writeFile(outputPath, imageData);
}