Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions packages/cli/src/lib/fs-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import fs from 'node:fs/promises';

// Small helper to retry copyFile on transient errors (EBUSY on Windows CI).
// Attempts: number of tries (default 5)
// baseDelay: starting delay in ms for exponential backoff (default 100ms)
async function copyFileWithRetry(source, target, { attempts = 5, baseDelay = 100 } = {}) {
let lastError;

for (let i = 0; i < attempts; i++) {
try {
return await fs.copyFile(source, target);
} catch (err) {
lastError = err;

// Only retry for transient EBUSY / EPERM on Windows or generic resource busy errors
if ((err.code === 'EBUSY' || err.code === 'EPERM' || err.code === 'EACCES') && i < attempts - 1) {
const delay = baseDelay * Math.pow(2, i);
// add small jitter
const jitter = Math.floor(Math.random() * baseDelay);
await new Promise((res) => setTimeout(res, delay + jitter));
continue;
}

// non-retryable or last attempt -> rethrow
throw err;
}
}

// if we exhausted loop, throw last seen error
throw lastError;
}

export { copyFileWithRetry };
6 changes: 3 additions & 3 deletions packages/cli/src/lifecycles/copy.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import { checkResourceExists } from "../lib/resource-utils.js";
import { asyncForEach } from "../lib/async-utils.js";
import { copyFileWithRetry } from "../lib/fs-utils.js";

async function rreaddir(dir, allFiles = []) {
const files = (await fs.readdir(dir)).map((f) => new URL(`./${f}`, dir));
Expand All @@ -20,10 +21,9 @@ async function rreaddir(dir, allFiles = []) {
async function copyFile(source, target, projectDirectory) {
try {
console.info(`copying file... ${source.pathname.replace(projectDirectory.pathname, "")}`);

await fs.copyFile(source, target);
await copyFileWithRetry(source, target);
} catch (error) {
console.error("ERROR", error);
console.error("ERROR copying file", source.href, "->", target.href, error);
}
}

Expand Down
7 changes: 6 additions & 1 deletion packages/cli/src/plugins/resource/plugin-standard-css.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*
*/
import fs from "node:fs";
import { copyFileWithRetry } from "../../lib/fs-utils.js";
import path from "node:path";
import { parse, walk } from "css-tree";
import { hashString } from "../../lib/hashing-utils.js";
Expand Down Expand Up @@ -134,7 +135,11 @@ function bundleCss(body, sourceUrl, compilation, workingUrl) {
recursive: true,
});

fs.promises.copyFile(resolvedUrl, new URL(`.${finalValue}`, outputDir));
// Use copy helper with retry to avoid intermittent EBUSY on Windows CI
// bundleCss is synchronous, so we don't await here; log any copy errors.
copyFileWithRetry(resolvedUrl, new URL(`.${finalValue}`, outputDir)).catch((err) =>
console.error('ERROR copying asset during CSS bundling', resolvedUrl.href, err),
);
}

optimizedCss += `url('${basePath}${finalValue}')`;
Expand Down
27 changes: 26 additions & 1 deletion packages/init/src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,31 @@ import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";

// synchronous copy with simple retry/backoff for Windows transient EBUSY
function copyFileSyncWithRetry(src, dest, attempts = 5, baseDelay = 50) {
let lastErr;

for (let i = 0; i < attempts; i++) {
try {
return fs.copyFileSync(src, dest);
} catch (err) {
lastErr = err;
if ((err.code === 'EBUSY' || err.code === 'EPERM' || err.code === 'EACCES') && i < attempts - 1) {
const delay = baseDelay * Math.pow(2, i);
const waitUntil = Date.now() + delay + Math.floor(Math.random() * baseDelay);
while (Date.now() < waitUntil) {
// busy wait — this is synchronous intentionally
}
continue;
}

throw err;
}
}

throw lastErr;
}

function copyTemplate(templateDirUrl, outputDirUrl) {
console.log("copying project files to => ", outputDirUrl.pathname);

Expand All @@ -16,7 +41,7 @@ function copyTemplate(templateDirUrl, outputDirUrl) {
if (isDir && !fs.existsSync(outputFileUrl)) {
fs.mkdirSync(outputFileUrl);
} else if (!isDir) {
fs.copyFileSync(templateFileUrl, outputFileUrl);
copyFileSyncWithRetry(templateFileUrl, outputFileUrl);
}
});
}
Expand Down