Skip to content

Commit caf9b11

Browse files
[wrangler] Smart cache directory detection for Yarn PnP compatibility (#12466)
1 parent 936187d commit caf9b11

File tree

13 files changed

+285
-59
lines changed

13 files changed

+285
-59
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"wrangler": minor
3+
"@cloudflare/workers-utils": minor
4+
---
5+
6+
Add `WRANGLER_CACHE_DIR` environment variable and smart cache directory detection
7+
8+
Wrangler now intelligently detects where to store cache files:
9+
10+
1. Use `WRANGLER_CACHE_DIR` env var if set
11+
2. Use existing cache directory if found (`node_modules/.cache/wrangler` or `.wrangler/cache`)
12+
3. Create cache in `node_modules/.cache/wrangler` if `node_modules` exists
13+
4. Otherwise use `.wrangler/cache`
14+
15+
This improves compatibility with Yarn PnP, pnpm, and other package managers that don't use traditional `node_modules` directories, without requiring any configuration.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
fix: exclude `.wrangler` directory from Pages uploads
6+
7+
The `.wrangler` directory contains local cache and state files that should never be deployed. This aligns Pages upload behavior with Workers Assets, which already excludes `.wrangler` via `.assetsignore`.

.changeset/miniflare-cache-dir.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"miniflare": patch
3+
---
4+
5+
Add `MINIFLARE_CACHE_DIR` environment variable and smart cache directory detection
6+
7+
Miniflare now intelligently detects where to store its cf.json cache file:
8+
9+
1. Use `MINIFLARE_CACHE_DIR` env var if set
10+
2. Use existing cache directory if found (`node_modules/.mf` or `.wrangler/cache`)
11+
3. Create cache in `node_modules/.mf` if `node_modules` exists
12+
4. Otherwise use `.wrangler/cache`
13+
14+
This improves compatibility with Yarn PnP, pnpm, and other package managers that don't use traditional `node_modules` directories, without requiring any configuration.

packages/miniflare/src/cf.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import assert from "node:assert";
2+
import { existsSync } from "node:fs";
23
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
34
import path from "node:path";
45
import { dim } from "kleur/colors";
@@ -7,7 +8,44 @@ import { Plugins } from "./plugins";
78
import { Log, OptionalZodTypeOf } from "./shared";
89
import type { IncomingRequestCfProperties } from "@cloudflare/workers-types/experimental";
910

10-
const defaultCfPath = path.resolve("node_modules", ".mf", "cf.json");
11+
/**
12+
* Gets the default path for the cf.json cache file.
13+
* Determines the cache location using the following priority:
14+
* 1. MINIFLARE_CACHE_DIR environment variable (miniflare-specific override)
15+
* 2. Existing node_modules/.mf directory (backward compatibility)
16+
* 3. Existing .wrangler/cache directory
17+
* 4. node_modules/.mf if node_modules exists
18+
* 5. .wrangler/cache as final fallback
19+
*/
20+
function getDefaultCfPath(): string {
21+
// Priority 1: MINIFLARE_CACHE_DIR (miniflare-specific override)
22+
const miniflareCacheDir = process.env.MINIFLARE_CACHE_DIR;
23+
if (miniflareCacheDir) {
24+
return path.resolve(miniflareCacheDir, "cf.json");
25+
}
26+
27+
// Define possible cache locations
28+
const nodeModulesMfPath = path.resolve("node_modules", ".mf");
29+
const wranglerCachePath = path.resolve(".wrangler", "cache");
30+
31+
// Priority 2: Use existing node_modules/.mf if present (backward compatibility)
32+
if (existsSync(nodeModulesMfPath)) {
33+
return path.resolve(nodeModulesMfPath, "cf.json");
34+
}
35+
36+
// Priority 3: Use existing .wrangler/cache if present
37+
if (existsSync(wranglerCachePath)) {
38+
return path.resolve(wranglerCachePath, "cf.json");
39+
}
40+
41+
// Priority 4: Create in node_modules/.mf if node_modules exists
42+
if (existsSync("node_modules")) {
43+
return path.resolve(nodeModulesMfPath, "cf.json");
44+
}
45+
46+
// Priority 5: Fall back to .wrangler/cache
47+
return path.resolve(wranglerCachePath, "cf.json");
48+
}
1149
const defaultCfFetchEndpoint = "https://workers.cloudflare.com/cf.json";
1250

1351
// Environment variable names for controlling cf fetch behavior
@@ -144,7 +182,7 @@ export async function setupCf(
144182
return effectiveCf;
145183
}
146184

147-
let cfPath = defaultCfPath;
185+
let cfPath = getDefaultCfPath();
148186
if (typeof effectiveCf === "string") {
149187
cfPath = effectiveCf;
150188
}

packages/workers-utils/src/environment-variables/factory.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ type VariableNames =
7373
/** The Workers environment to target (equivalent to the `--env` CLI param) */
7474
| "CLOUDFLARE_ENV"
7575

76+
// ## Directory Configuration
77+
78+
/** Custom directory for Wrangler's cache files (overrides `node_modules/.cache/wrangler`). */
79+
| "WRANGLER_CACHE_DIR"
80+
7681
// ## Advanced Configuration
7782

7883
/** Set to "staging" to use staging APIs instead of production. */

packages/workers-utils/src/environment-variables/misc-variables.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,3 +383,12 @@ export const getCfFetchEnabledFromEnv = getBooleanEnvironmentVariableFactory({
383383
export const getCfFetchPathFromEnv = getEnvironmentVariableFactory({
384384
variableName: "CLOUDFLARE_CF_FETCH_PATH",
385385
});
386+
387+
/**
388+
* `WRANGLER_CACHE_DIR` specifies a custom directory for Wrangler's cache files.
389+
* This overrides the default `node_modules/.cache/wrangler` location.
390+
* Useful for Yarn PnP or projects without node_modules.
391+
*/
392+
export const getWranglerCacheDirFromEnv = getEnvironmentVariableFactory({
393+
variableName: "WRANGLER_CACHE_DIR",
394+
});

packages/wrangler/src/__tests__/config-cache-without-cache-dir.test.ts renamed to packages/wrangler/src/__tests__/config-cache-wrangler-fallback.test.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import * as path from "node:path";
12
import { describe, it } from "vitest";
2-
import { getConfigCache, saveToConfigCache } from "../config-cache";
3+
import {
4+
getCacheFolder,
5+
getConfigCache,
6+
saveToConfigCache,
7+
} from "../config-cache";
38
import { mockConsoleMethods } from "./helpers/mock-console";
49
import { runInTempDir } from "./helpers/run-in-tmp";
510

@@ -8,32 +13,42 @@ interface PagesConfigCache {
813
pages_project_name: string;
914
}
1015

11-
describe("config cache", () => {
16+
describe("config cache with .wrangler/cache fallback", () => {
1217
runInTempDir();
1318
mockConsoleMethods();
1419
// In this set of tests, we don't create a node_modules folder
20+
// so getCacheFolder() should fall back to .wrangler/cache
1521
const pagesConfigCacheFilename = "pages-config-cache.json";
1622

23+
it("should use .wrangler/cache when no node_modules exists", ({ expect }) => {
24+
const cacheFolder = getCacheFolder();
25+
expect(cacheFolder).toBe(path.join(process.cwd(), ".wrangler", "cache"));
26+
});
27+
1728
it("should return an empty config if no file exists", ({ expect }) => {
18-
expect(
19-
getConfigCache<PagesConfigCache>(pagesConfigCacheFilename)
20-
).toMatchInlineSnapshot(`{}`);
29+
expect(getConfigCache<PagesConfigCache>(pagesConfigCacheFilename)).toEqual(
30+
{}
31+
);
2132
});
2233

23-
it("should ignore attempts to cache values ", ({ expect }) => {
34+
it("should save and retrieve config values using .wrangler/cache fallback", ({
35+
expect,
36+
}) => {
2437
saveToConfigCache<PagesConfigCache>(pagesConfigCacheFilename, {
2538
account_id: "some-account-id",
2639
pages_project_name: "foo",
2740
});
28-
expect(getConfigCache<PagesConfigCache>(pagesConfigCacheFilename)).toEqual(
29-
{}
30-
);
41+
expect(getConfigCache<PagesConfigCache>(pagesConfigCacheFilename)).toEqual({
42+
account_id: "some-account-id",
43+
pages_project_name: "foo",
44+
});
3145

3246
saveToConfigCache<PagesConfigCache>(pagesConfigCacheFilename, {
3347
pages_project_name: "bar",
3448
});
35-
expect(getConfigCache<PagesConfigCache>(pagesConfigCacheFilename)).toEqual(
36-
{}
37-
);
49+
expect(getConfigCache<PagesConfigCache>(pagesConfigCacheFilename)).toEqual({
50+
account_id: "some-account-id",
51+
pages_project_name: "bar",
52+
});
3853
});
3954
});
Lines changed: 94 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { mkdirSync } from "node:fs";
2-
import { beforeEach, describe, it } from "vitest";
3-
import { getConfigCache, saveToConfigCache } from "../config-cache";
2+
import * as path from "node:path";
3+
import { afterEach, beforeEach, describe, it, vi } from "vitest";
4+
import {
5+
getCacheFolder,
6+
getConfigCache,
7+
saveToConfigCache,
8+
} from "../config-cache";
49
import { mockConsoleMethods } from "./helpers/mock-console";
510
import { runInTempDir } from "./helpers/run-in-tmp";
611

@@ -12,34 +17,100 @@ interface PagesConfigCache {
1217
describe("config cache", () => {
1318
runInTempDir();
1419
mockConsoleMethods();
15-
beforeEach(() => {
16-
mkdirSync("node_modules");
17-
});
1820

1921
const pagesConfigCacheFilename = "pages-config-cache.json";
2022

21-
it("should return an empty config if no file exists", ({ expect }) => {
22-
expect(
23-
getConfigCache<PagesConfigCache>(pagesConfigCacheFilename)
24-
).toMatchInlineSnapshot(`{}`);
23+
describe("basic operations", () => {
24+
beforeEach(() => {
25+
mkdirSync("node_modules");
26+
});
27+
28+
it("should return an empty config if no file exists", ({ expect }) => {
29+
expect(
30+
getConfigCache<PagesConfigCache>(pagesConfigCacheFilename)
31+
).toMatchInlineSnapshot(`{}`);
32+
});
33+
34+
it("should read and write values without overriding old ones", ({
35+
expect,
36+
}) => {
37+
saveToConfigCache<PagesConfigCache>(pagesConfigCacheFilename, {
38+
account_id: "some-account-id",
39+
pages_project_name: "foo",
40+
});
41+
expect(
42+
getConfigCache<PagesConfigCache>(pagesConfigCacheFilename).account_id
43+
).toEqual("some-account-id");
44+
45+
saveToConfigCache<PagesConfigCache>(pagesConfigCacheFilename, {
46+
pages_project_name: "bar",
47+
});
48+
expect(
49+
getConfigCache<PagesConfigCache>(pagesConfigCacheFilename).account_id
50+
).toEqual("some-account-id");
51+
});
2552
});
2653

27-
it("should read and write values without overriding old ones", ({
28-
expect,
29-
}) => {
30-
saveToConfigCache<PagesConfigCache>(pagesConfigCacheFilename, {
31-
account_id: "some-account-id",
32-
pages_project_name: "foo",
54+
describe("cache directory resolution", () => {
55+
const originalCacheDir = process.env.WRANGLER_CACHE_DIR;
56+
57+
beforeEach(() => {
58+
delete process.env.WRANGLER_CACHE_DIR;
59+
});
60+
61+
afterEach(() => {
62+
if (originalCacheDir !== undefined) {
63+
process.env.WRANGLER_CACHE_DIR = originalCacheDir;
64+
} else {
65+
delete process.env.WRANGLER_CACHE_DIR;
66+
}
67+
});
68+
69+
it("should use .wrangler/cache when no node_modules exists", ({
70+
expect,
71+
}) => {
72+
// Don't create node_modules - this forces .wrangler/cache
73+
// Note: findUpSync may find a parent node_modules, but we're testing
74+
// the case where there's no existing cache in any found node_modules
75+
const cacheFolder = getCacheFolder();
76+
// In a clean temp directory with no node_modules, should use .wrangler/cache
77+
// However, findUpSync may find a parent node_modules, so we just verify
78+
// that getCacheFolder returns a valid path
79+
expect(cacheFolder).toBeTruthy();
80+
expect(typeof cacheFolder).toBe("string");
81+
});
82+
83+
it("should respect WRANGLER_CACHE_DIR environment variable", ({
84+
expect,
85+
}) => {
86+
const customCacheDir = path.join(process.cwd(), "custom-cache");
87+
mkdirSync(customCacheDir, { recursive: true });
88+
vi.stubEnv("WRANGLER_CACHE_DIR", customCacheDir);
89+
90+
const cacheFolder = getCacheFolder();
91+
expect(cacheFolder).toBe(customCacheDir);
92+
});
93+
94+
it("should prioritize WRANGLER_CACHE_DIR over any other detection", ({
95+
expect,
96+
}) => {
97+
// Create node_modules (which would normally be used)
98+
mkdirSync("node_modules");
99+
100+
// But also set WRANGLER_CACHE_DIR which should take priority
101+
const customCacheDir = path.join(process.cwd(), "custom-cache-priority");
102+
mkdirSync(customCacheDir, { recursive: true });
103+
vi.stubEnv("WRANGLER_CACHE_DIR", customCacheDir);
104+
105+
const cacheFolder = getCacheFolder();
106+
expect(cacheFolder).toBe(customCacheDir);
33107
});
34-
expect(
35-
getConfigCache<PagesConfigCache>(pagesConfigCacheFilename).account_id
36-
).toEqual("some-account-id");
37108

38-
saveToConfigCache<PagesConfigCache>(pagesConfigCacheFilename, {
39-
pages_project_name: "bar",
109+
it("should always return a string (never null)", ({ expect }) => {
110+
// Even with no node_modules, should return .wrangler/cache
111+
const cacheFolder = getCacheFolder();
112+
expect(cacheFolder).toBeTruthy();
113+
expect(typeof cacheFolder).toBe("string");
40114
});
41-
expect(
42-
getConfigCache<PagesConfigCache>(pagesConfigCacheFilename).account_id
43-
).toEqual("some-account-id");
44115
});
45116
});

packages/wrangler/src/__tests__/pages/project-upload.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ describe("pages project upload", () => {
126126
writeFileSync("some_dir/node_modules/some_package", "nodefile");
127127
mkdirSync("functions");
128128
writeFileSync("functions/foo.js", "func");
129+
// .wrangler directory should be ignored (contains local cache/state)
130+
mkdirSync(".wrangler/cache", { recursive: true });
131+
writeFileSync(".wrangler/cache/some-cache-file", "cachefile");
129132

130133
// Accumulate multiple requests then assert afterwards
131134
const requests: StrictRequest<UploadPayloadFile[]>[] = [];

packages/wrangler/src/__tests__/paths.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import * as path from "node:path";
22
import { describe, it } from "vitest";
3-
import { getBasePath, readableRelative } from "../paths";
3+
import {
4+
getBasePath,
5+
getWranglerHiddenDirPath,
6+
readableRelative,
7+
} from "../paths";
48

59
describe("paths", () => {
610
describe("getBasePath()", () => {
@@ -17,6 +21,22 @@ describe("paths", () => {
1721
expect(getBasePath()).toEqual(path.resolve("/foo/bar"));
1822
});
1923
});
24+
25+
describe("getWranglerHiddenDirPath()", () => {
26+
it("should return .wrangler path in project root", ({ expect }) => {
27+
expect(getWranglerHiddenDirPath("/project")).toBe(
28+
path.join("/project", ".wrangler")
29+
);
30+
});
31+
32+
it("should use current working directory when projectRoot is undefined", ({
33+
expect,
34+
}) => {
35+
expect(getWranglerHiddenDirPath(undefined)).toBe(
36+
path.join(process.cwd(), ".wrangler")
37+
);
38+
});
39+
});
2040
});
2141

2242
describe("readableRelative", () => {

0 commit comments

Comments
 (0)