Merge pull request #435 from sivdead/fix/dev-assets-directory

Create missing Wrangler assets directories before development startup. Add regression coverage for creation before CLI invocation, preservation of existing assets, and non-dev command behavior.

Validation: typecheck, typecheck:mobile, build:web, and all 2244 non-E2E tests passed.
This commit is contained in:
tianma-if committed 2026-09-26 11:57:51 +08:00
commit 509d523f00
3 files changed
+77 -3

No files matched your search

+10 -2
View File
@@ -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);
+7
View File
@@ -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}`,
+60 -1
View File
@@ -1,5 +1,7 @@
import { describe, expect, test } from "bun:test";
import { globSync, readFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { existsSync, globSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { resolve, sep } from "node:path";
import {
buildLocalDevEnvironmentFile,
@@ -12,6 +14,7 @@ import {
normalizeD1MigrationSql,
productionVersionIds,
LOCAL_DEV_CREDENTIALS_ENCRYPTION_KEY,
resolveWranglerAssetsDirectory,
resolveWranglerCliPath,
resolveWranglerRuntimeExecutable,
runWranglerSync,
@@ -73,6 +76,62 @@ 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("prepares missing dev assets before Wrangler starts and preserves existing files", () => {
const workingDirectory = mkdtempSync(resolve(tmpdir(), "edgeever-dev-assets-"));
const assetsDirectory = resolve(workingDirectory, "custom-assets", "dist");
const configPath = resolve(workingDirectory, "custom.toml");
const cliDirectory = resolve(workingDirectory, "node_modules", "wrangler", "bin");
const env = Object.fromEntries(
["PATH", "Path", "PATHEXT", "SystemRoot", "ComSpec", "TEMP", "TMP"]
.map((name) => [name, process.env[name]])
.filter((entry): entry is [string, string] => Boolean(entry[1])),
);
const run = (...args: string[]) => spawnSync(
"node",
[resolve(import.meta.dir, "../scripts/run-wrangler.mjs"), ...args],
{
cwd: workingDirectory,
encoding: "utf8",
env: { ...env, WRANGLER_CONFIG: configPath },
},
);
try {
mkdirSync(cliDirectory, { recursive: true });
writeFileSync(configPath, 'name = "dev-assets-test"\n[assets]\ndirectory = "custom-assets/dist"\n');
writeFileSync(resolve(cliDirectory, "wrangler.js"), [
'const { statSync } = require("node:fs");',
'if (process.argv.includes("dev") && !statSync("custom-assets/dist").isDirectory()) process.exit(1);',
].join("\n"));
// Non-dev commands must not manufacture a missing build directory.
const nonDev = run("d1", "execute", "DB", "--local", "--command", "SELECT 1");
expect(nonDev.error).toBeUndefined();
expect(nonDev.status).toBe(0);
expect(existsSync(assetsDirectory)).toBe(false);
const firstStart = run("dev", "--local");
expect(firstStart.error).toBeUndefined();
expect(firstStart.status).toBe(0);
expect(existsSync(assetsDirectory)).toBe(true);
const existingAsset = resolve(assetsDirectory, "index.html");
writeFileSync(existingAsset, "existing build must survive dev startup");
const secondStart = run("dev", "--local");
expect(secondStart.status).toBe(0);
expect(readFileSync(existingAsset, "utf8")).toBe("existing build must survive dev startup");
} finally {
rmSync(workingDirectory, { recursive: true, force: true });
}
});
test("resolves an exact D1 database name from Wrangler JSON", () => {
const databases = JSON.stringify([
{ uuid: "11111111-1111-1111-1111-111111111111", name: "another-database" },