From bae8067c604092052e33692ea1215451a334307d Mon Sep 17 00:00:00 2001 From: AiAlchemist0 Date: Thu, 20 Aug 2026 09:42:41 -0700 Subject: [PATCH 1/5] fix: skip mkdir when C3 parent dir already exists --- .changeset/c3-mkdir-drive-root.md | 5 +++++ packages/create-cloudflare/src/cli.ts | 8 +++++--- 2 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 .changeset/c3-mkdir-drive-root.md diff --git a/.changeset/c3-mkdir-drive-root.md b/.changeset/c3-mkdir-drive-root.md new file mode 100644 index 00000000000..cd98fca1d19 --- /dev/null +++ b/.changeset/c3-mkdir-drive-root.md @@ -0,0 +1,5 @@ +--- +"create-cloudflare": patch +--- + +Skip `mkdir` when the project parent directory already exists, so `create-cloudflare` works at a Windows drive root (`E:\`) instead of throwing `EPERM`. diff --git a/packages/create-cloudflare/src/cli.ts b/packages/create-cloudflare/src/cli.ts index 9a1fc24adac..9e0f76114d6 100644 --- a/packages/create-cloudflare/src/cli.ts +++ b/packages/create-cloudflare/src/cli.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { mkdirSync } from "node:fs"; +import { existsSync, mkdirSync } from "node:fs"; import { dirname } from "node:path"; import { chdir } from "node:process"; import { @@ -125,8 +125,10 @@ export const setupProjectDirectory = (ctx: C3Context) => { const directory = dirname(path); - // If the target is a nested directory, create the parent - mkdirSync(directory, { recursive: true }); + // Creating a Windows drive root (`E:\`) throws EPERM. Skip if it already exists. + if (!existsSync(directory)) { + mkdirSync(directory, { recursive: true }); + } // Change to the parent directory chdir(directory); From a2686e67270c4cbf9b5cc4a8ae973543ffceb263 Mon Sep 17 00:00:00 2001 From: AiAlchemist0 Date: Thu, 20 Aug 2026 11:01:45 -0700 Subject: [PATCH 2/5] test: cover C3 mkdir skip when parent exists --- .../src/__tests__/project-directory.test.ts | 50 +++++++++++++++++++ packages/create-cloudflare/src/cli.ts | 23 +-------- .../src/project-directory.ts | 22 ++++++++ 3 files changed, 73 insertions(+), 22 deletions(-) create mode 100644 packages/create-cloudflare/src/__tests__/project-directory.test.ts create mode 100644 packages/create-cloudflare/src/project-directory.ts diff --git a/packages/create-cloudflare/src/__tests__/project-directory.test.ts b/packages/create-cloudflare/src/__tests__/project-directory.test.ts new file mode 100644 index 00000000000..997c509dcc2 --- /dev/null +++ b/packages/create-cloudflare/src/__tests__/project-directory.test.ts @@ -0,0 +1,50 @@ +import { existsSync, mkdirSync } from "node:fs"; +import { chdir } from "node:process"; +import { resolve } from "node:path"; +import { beforeEach, describe, test, vi } from "vitest"; +import { setupProjectDirectory } from "../project-directory"; +import type { C3Context } from "types"; + +vi.mock("node:fs"); +vi.mock("node:process", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, chdir: vi.fn() }; +}); + +const ctxFor = (projectPath: string): C3Context => + ({ + args: {}, + project: { name: "my-app", path: projectPath }, + template: {}, + deployment: {}, + originalCWD: "/", + gitRepoAlreadyExisted: false, + }) as unknown as C3Context; + +describe("setupProjectDirectory", () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + test("does not mkdir when the parent already exists", ({ expect }) => { + const projectPath = resolve("already-there", "my-app"); + const parent = resolve("already-there"); + vi.mocked(existsSync).mockImplementation((p) => String(p) === parent); + + setupProjectDirectory(ctxFor(projectPath)); + + expect(mkdirSync).not.toHaveBeenCalled(); + expect(chdir).toHaveBeenCalledWith(parent); + }); + + test("creates the parent when it is missing", ({ expect }) => { + const projectPath = resolve("new-parent", "my-app"); + const parent = resolve("new-parent"); + vi.mocked(existsSync).mockReturnValue(false); + + setupProjectDirectory(ctxFor(projectPath)); + + expect(mkdirSync).toHaveBeenCalledWith(parent, { recursive: true }); + expect(chdir).toHaveBeenCalledWith(parent); + }); +}); diff --git a/packages/create-cloudflare/src/cli.ts b/packages/create-cloudflare/src/cli.ts index 9e0f76114d6..925e2ab665d 100644 --- a/packages/create-cloudflare/src/cli.ts +++ b/packages/create-cloudflare/src/cli.ts @@ -1,6 +1,4 @@ #!/usr/bin/env node -import { existsSync, mkdirSync } from "node:fs"; -import { dirname } from "node:path"; import { chdir } from "node:process"; import { cancel, @@ -40,7 +38,7 @@ import { updatePackageScripts, writeAgentsMd, } from "./templates"; -import { validateProjectDirectory } from "./validators"; +import { setupProjectDirectory } from "./project-directory"; import { addTypes } from "./workers"; import { updateWranglerConfig } from "./wrangler/config"; import type { C3Args, C3Context } from "types"; @@ -115,25 +113,6 @@ export const runCli = async (args: Partial) => { logRaw(""); }; -export const setupProjectDirectory = (ctx: C3Context) => { - // Crash if the directory already exists - const path = ctx.project.path; - const err = validateProjectDirectory(path, ctx.args); - if (err) { - throw new Error(err); - } - - const directory = dirname(path); - - // Creating a Windows drive root (`E:\`) throws EPERM. Skip if it already exists. - if (!existsSync(directory)) { - mkdirSync(directory, { recursive: true }); - } - - // Change to the parent directory - chdir(directory); -}; - const create = async (ctx: C3Context) => { const { template } = ctx; diff --git a/packages/create-cloudflare/src/project-directory.ts b/packages/create-cloudflare/src/project-directory.ts new file mode 100644 index 00000000000..d553408df0d --- /dev/null +++ b/packages/create-cloudflare/src/project-directory.ts @@ -0,0 +1,22 @@ +import { existsSync, mkdirSync } from "node:fs"; +import { dirname } from "node:path"; +import { chdir } from "node:process"; +import { validateProjectDirectory } from "./validators"; +import type { C3Context } from "types"; + +export const setupProjectDirectory = (ctx: C3Context) => { + const path = ctx.project.path; + const err = validateProjectDirectory(path, ctx.args); + if (err) { + throw new Error(err); + } + + const directory = dirname(path); + + // Creating a Windows drive root (`E:\`) throws EPERM. Skip if it already exists. + if (!existsSync(directory)) { + mkdirSync(directory, { recursive: true }); + } + + chdir(directory); +}; From c459ce39ae1013da6c43019b3937c50a09e74730 Mon Sep 17 00:00:00 2001 From: AiAlchemist0 Date: Thu, 20 Aug 2026 11:48:23 -0700 Subject: [PATCH 3/5] docs: add JSDoc on setupProjectDirectory --- packages/create-cloudflare/src/project-directory.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/create-cloudflare/src/project-directory.ts b/packages/create-cloudflare/src/project-directory.ts index d553408df0d..c11e337e8e3 100644 --- a/packages/create-cloudflare/src/project-directory.ts +++ b/packages/create-cloudflare/src/project-directory.ts @@ -4,6 +4,13 @@ import { chdir } from "node:process"; import { validateProjectDirectory } from "./validators"; import type { C3Context } from "types"; +/** + * Validates the target project directory and ensures its parent exists before + * changing into it. Skips `mkdir` when the parent already exists so a Windows + * drive root (`E:\`) does not throw `EPERM`. + * + * @param ctx - The C3 context containing the resolved project path and args + */ export const setupProjectDirectory = (ctx: C3Context) => { const path = ctx.project.path; const err = validateProjectDirectory(path, ctx.args); From 4d8a0bc9f4956fcf5e5d01c224de9c6a8164fabc Mon Sep 17 00:00:00 2001 From: AiAlchemist0 Date: Thu, 20 Aug 2026 17:34:38 -0700 Subject: [PATCH 4/5] style: oxfmt project-directory test imports --- .../create-cloudflare/src/__tests__/project-directory.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-cloudflare/src/__tests__/project-directory.test.ts b/packages/create-cloudflare/src/__tests__/project-directory.test.ts index 997c509dcc2..0b7c7b84ddd 100644 --- a/packages/create-cloudflare/src/__tests__/project-directory.test.ts +++ b/packages/create-cloudflare/src/__tests__/project-directory.test.ts @@ -1,6 +1,6 @@ import { existsSync, mkdirSync } from "node:fs"; -import { chdir } from "node:process"; import { resolve } from "node:path"; +import { chdir } from "node:process"; import { beforeEach, describe, test, vi } from "vitest"; import { setupProjectDirectory } from "../project-directory"; import type { C3Context } from "types"; From b9203049fa21cb7f3bd0dbc68b5f05a0665b5a85 Mon Sep 17 00:00:00 2001 From: AiAlchemist0 Date: Thu, 20 Aug 2026 23:02:33 -0700 Subject: [PATCH 5/5] style: sort project-directory import in cli.ts --- packages/create-cloudflare/src/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-cloudflare/src/cli.ts b/packages/create-cloudflare/src/cli.ts index 925e2ab665d..a414d695786 100644 --- a/packages/create-cloudflare/src/cli.ts +++ b/packages/create-cloudflare/src/cli.ts @@ -31,6 +31,7 @@ import { gitCommit, offerGit } from "./git"; import { showHelp } from "./help"; import { reporter, runTelemetryCommand } from "./metrics"; import { createProject } from "./pages"; +import { setupProjectDirectory } from "./project-directory"; import { copyTemplateFiles, createContext, @@ -38,7 +39,6 @@ import { updatePackageScripts, writeAgentsMd, } from "./templates"; -import { setupProjectDirectory } from "./project-directory"; import { addTypes } from "./workers"; import { updateWranglerConfig } from "./wrangler/config"; import type { C3Args, C3Context } from "types";