From 25e4a9173903afe0b2089849f6050fb89678c2dc Mon Sep 17 00:00:00 2001 From: sivdead Date: Fri, 25 Sep 2026 11:15:28 +0800 Subject: [PATCH] fix(dev): create the Wrangler assets directory before `wrangler dev` Wrangler refuses to start `dev` when the configured `[assets].directory` is missing and offers no option to create it. Local development serves the web app from Vite, so `apps/web/dist` only exists after a previous `build:web`; a fresh clone or `git clean` made `bun run dev` fail. Resolve the directory from the effective Wrangler config (so custom configs via WRANGLER_CONFIG are honored) and create it for every `dev` command routed through run-wrangler. Deploy and migration commands are unchanged. Co-Authored-By: Claude Opus 5.5 --- scripts/run-wrangler.mjs | 12 ++++++++++-- scripts/wrangler-runner.mjs | 7 +++++++ tests/wrangler-runner.test.ts | 8 ++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/scripts/run-wrangler.mjs b/scripts/run-wrangler.mjs index ce4f3af5..cd9d4314 100644 --- a/scripts/run-wrangler.mjs +++ b/scripts/run-wrangler.mjs @@ -19,6 +19,7 @@ import { PLACEHOLDER_D1_ID, productionVersionIds, repositoryWranglerConfigError, + resolveWranglerAssetsDirectory, runWranglerSync, shouldCaptureDeploymentTargets, } from "./wrangler-runner.mjs"; @@ -166,8 +167,9 @@ const envValue = (name) => { const isRemoteCommand = wranglerArgs.includes("deploy") || wranglerArgs.includes("--remote"); const isDeployCommand = wranglerArgs.includes("deploy"); -const isRemoteDevCommand = wranglerArgs.includes("dev") && wranglerArgs.includes("--remote"); -const isLocalDevCommand = wranglerArgs.includes("dev") && wranglerArgs.includes("--local"); +const isDevCommand = wranglerArgs.includes("dev"); +const isRemoteDevCommand = isDevCommand && wranglerArgs.includes("--remote"); +const isLocalDevCommand = isDevCommand && wranglerArgs.includes("--local"); // Any --local command rewrites .wrangler.generated.toml. Keep local-only vars // (especially auth-free access) so `d1 migrations apply --local` cannot strip // them and leave a later wrangler reload requiring login mid-session. @@ -442,6 +444,12 @@ if (isDeployCommand && Object.keys(authSecrets).length === 0 && useExistingAuthS writeWranglerNotice("info", "using the authentication Secret provisioned by Cloudflare"); } +if (isDevCommand) { + // Local dev serves the web app from Vite, so an empty assets directory is enough. + const assetsDirectory = resolveWranglerAssetsDirectory(config, dirname(configPath)); + if (assetsDirectory) mkdirSync(assetsDirectory, { recursive: true }); +} + if (isLocalDevCommand && !hasEnvFileArg) { writeFileSync(generatedLocalDevEnvPath, buildLocalDevEnvironmentFile()); finalWranglerArgs.push("--env-file", generatedLocalDevEnvPath); diff --git a/scripts/wrangler-runner.mjs b/scripts/wrangler-runner.mjs index e14a6e66..16df23fc 100644 --- a/scripts/wrangler-runner.mjs +++ b/scripts/wrangler-runner.mjs @@ -116,6 +116,13 @@ export const shouldCaptureDeploymentTargets = (env = process.env) => export const LOCAL_DEV_CREDENTIALS_ENCRYPTION_KEY = "edgeever-local-development-credentials-key-v1"; +// Wrangler refuses to start `dev` when the assets directory is missing and has no +// option to create it. Like Wrangler, resolve `[assets].directory` from the config's directory. +export const resolveWranglerAssetsDirectory = (config, configDirectory) => { + const match = config.match(/^\s*\[assets\][^[]*?^\s*directory\s*=\s*["']([^"']+)["']/m); + return match ? resolve(configDirectory, match[1]) : null; +}; + export const buildLocalDevEnvironmentFile = () => [ "# Local-only values. Remote instance secrets are intentionally excluded.", `EDGE_EVER_CREDENTIALS_ENCRYPTION_KEY=${LOCAL_DEV_CREDENTIALS_ENCRYPTION_KEY}`, diff --git a/tests/wrangler-runner.test.ts b/tests/wrangler-runner.test.ts index 871d08fe..7630f67f 100644 --- a/tests/wrangler-runner.test.ts +++ b/tests/wrangler-runner.test.ts @@ -12,6 +12,7 @@ import { normalizeD1MigrationSql, productionVersionIds, LOCAL_DEV_CREDENTIALS_ENCRYPTION_KEY, + resolveWranglerAssetsDirectory, resolveWranglerCliPath, resolveWranglerRuntimeExecutable, runWranglerSync, @@ -73,6 +74,13 @@ describe("cross-platform Wrangler runner", () => { expect(envFile).not.toContain("EDGE_EVER_AUTH_PASSWORD_HASH="); }); + test("resolves the configured assets directory so local dev can create it", () => { + const config = '[vars]\ndirectory = "wrong"\n\n[assets]\ndirectory = "public"\n'; + + expect(resolveWranglerAssetsDirectory(config, "/repo")).toBe(resolve("/repo", "public")); + expect(resolveWranglerAssetsDirectory('name = "x"\n', "/repo")).toBeNull(); + }); + test("resolves an exact D1 database name from Wrangler JSON", () => { const databases = JSON.stringify([ { uuid: "11111111-1111-1111-1111-111111111111", name: "another-database" },