Skip to content

Commit 814d321

Browse files
[miniflare] Add environment variables to control cf.json fetching
Adds two new environment variables that allow users to control the cf.json caching behavior in Miniflare: - CLOUDFLARE_CF_FETCH_ENABLED: Set to "false" or "0" to disable fetching entirely (uses fallback data). Defaults to "true". - CLOUDFLARE_CF_FETCH_PATH: Set to a custom path to use a different location for caching the cf.json file. Empty string is treated as unset. This is useful for non-JavaScript projects (like Rust or Go Workers) that don't want a node_modules directory created automatically. Fixes #3659
1 parent c0b6152 commit 814d321

5 files changed

Lines changed: 275 additions & 6 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
"miniflare": minor
3+
---
4+
5+
Add environment variables to control cf.json fetching behavior
6+
7+
You can now use environment variables to control how Miniflare handles the `Request.cf` object caching:
8+
9+
- `CLOUDFLARE_CF_FETCH_ENABLED` - Set to "false" to disable fetching entirely and use fallback data. No `node_modules/.mf/cf.json` file will be created. Defaults to "true".
10+
- `CLOUDFLARE_CF_FETCH_PATH` - Set to a custom path to use a different location for caching the cf.json file instead of the default `node_modules/.mf/cf.json`.
11+
12+
This is particularly useful for non-JavaScript projects (like Rust or Go Workers) that don't want a `node_modules` directory created automatically.
13+
14+
Example:
15+
16+
```sh
17+
# Disable cf fetching for all projects
18+
export CLOUDFLARE_CF_FETCH_ENABLED=false
19+
npx wrangler dev
20+
21+
# Or use a custom cache location
22+
export CLOUDFLARE_CF_FETCH_PATH=/tmp/.cf-cache.json
23+
npx wrangler dev
24+
```

packages/miniflare/src/cf.ts

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import type { IncomingRequestCfProperties } from "@cloudflare/workers-types/expe
1010
const defaultCfPath = path.resolve("node_modules", ".mf", "cf.json");
1111
const defaultCfFetchEndpoint = "https://workers.cloudflare.com/cf.json";
1212

13+
// Environment variable names for controlling cf fetch behavior
14+
const CF_FETCH_ENABLED_ENV_VAR = "CLOUDFLARE_CF_FETCH_ENABLED";
15+
const CF_FETCH_PATH_ENV_VAR = "CLOUDFLARE_CF_FETCH_PATH";
16+
1317
export const fallbackCf: IncomingRequestCfProperties = {
1418
asOrganization: "",
1519
asn: 395747,
@@ -67,21 +71,82 @@ export const CF_DAYS = 30;
6771

6872
type CoreOptions = OptionalZodTypeOf<Plugins["core"]["sharedOptions"]>;
6973

74+
/**
75+
* Check if cf fetching is disabled via environment variable.
76+
*
77+
* Returns true if CLOUDFLARE_CF_FETCH_ENABLED is set to "false".
78+
*/
79+
function isCfFetchDisabledByEnv(): boolean {
80+
const envValue = process.env[CF_FETCH_ENABLED_ENV_VAR];
81+
if (envValue === undefined) {
82+
return false;
83+
}
84+
return envValue.toLowerCase() === "false";
85+
}
86+
87+
/**
88+
* Get custom cf.json path from environment variable.
89+
*
90+
* Returns the path if CLOUDFLARE_CF_FETCH_PATH is set and non-empty, otherwise undefined.
91+
*/
92+
function getCfPathFromEnv(): string | undefined {
93+
const envValue = process.env[CF_FETCH_PATH_ENV_VAR];
94+
// Treat empty string as unset (use default path)
95+
if (envValue === undefined || envValue === "") {
96+
return undefined;
97+
}
98+
return envValue;
99+
}
100+
101+
/**
102+
* Get the cf option value, considering environment variable overrides.
103+
*
104+
* Priority:
105+
* 1. If `cf` option is explicitly provided (not undefined), use it
106+
* 2. Check CLOUDFLARE_CF_FETCH_ENABLED environment variable:
107+
* - "false" -> return false (disable fetching)
108+
* 3. Check CLOUDFLARE_CF_FETCH_PATH environment variable:
109+
* - If set, use as custom path for cf.json cache
110+
* 4. Return undefined to use default behavior
111+
*/
112+
function getCfOptionWithEnvOverride(cf: CoreOptions["cf"]): CoreOptions["cf"] {
113+
// If cf option is explicitly provided, use it
114+
if (cf !== undefined) {
115+
return cf;
116+
}
117+
118+
// Check if fetching is disabled
119+
if (isCfFetchDisabledByEnv()) {
120+
return false;
121+
}
122+
123+
// Check for custom path
124+
const customPath = getCfPathFromEnv();
125+
if (customPath !== undefined) {
126+
return customPath;
127+
}
128+
129+
return undefined;
130+
}
131+
70132
export async function setupCf(
71133
log: Log,
72134
cf: CoreOptions["cf"]
73-
): Promise<Record<string, any>> {
74-
if (!(cf ?? process.env.NODE_ENV !== "test")) {
135+
): Promise<Record<string, unknown>> {
136+
// Apply environment variable override
137+
const effectiveCf = getCfOptionWithEnvOverride(cf);
138+
139+
if (!(effectiveCf ?? process.env.NODE_ENV !== "test")) {
75140
return fallbackCf;
76141
}
77142

78-
if (typeof cf === "object") {
79-
return cf;
143+
if (typeof effectiveCf === "object") {
144+
return effectiveCf;
80145
}
81146

82147
let cfPath = defaultCfPath;
83-
if (typeof cf === "string") {
84-
cfPath = cf;
148+
if (typeof effectiveCf === "string") {
149+
cfPath = effectiveCf;
85150
}
86151

87152
// Try load cfPath, if this fails, we'll catch the error and refetch.

packages/miniflare/test/cf.spec.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { Miniflare } from "miniflare";
2+
import { afterEach, beforeEach, describe, test } from "vitest";
3+
import { useDispose } from "./test-shared";
4+
5+
describe("CLOUDFLARE_CF_FETCH_ENABLED environment variable", () => {
6+
let originalEnabledEnv: string | undefined;
7+
let originalPathEnv: string | undefined;
8+
9+
beforeEach(() => {
10+
originalEnabledEnv = process.env.CLOUDFLARE_CF_FETCH_ENABLED;
11+
originalPathEnv = process.env.CLOUDFLARE_CF_FETCH_PATH;
12+
});
13+
14+
afterEach(() => {
15+
if (originalEnabledEnv === undefined) {
16+
delete process.env.CLOUDFLARE_CF_FETCH_ENABLED;
17+
} else {
18+
process.env.CLOUDFLARE_CF_FETCH_ENABLED = originalEnabledEnv;
19+
}
20+
if (originalPathEnv === undefined) {
21+
delete process.env.CLOUDFLARE_CF_FETCH_PATH;
22+
} else {
23+
process.env.CLOUDFLARE_CF_FETCH_PATH = originalPathEnv;
24+
}
25+
});
26+
27+
test("CLOUDFLARE_CF_FETCH_ENABLED=false disables cf fetching", async ({
28+
expect,
29+
}) => {
30+
process.env.CLOUDFLARE_CF_FETCH_ENABLED = "false";
31+
32+
const mf = new Miniflare({
33+
script: "",
34+
modules: true,
35+
});
36+
useDispose(mf);
37+
38+
const cf = await mf.getCf();
39+
// Should return fallback cf object when fetching is disabled
40+
expect(cf).toMatchObject({
41+
colo: "DFW",
42+
country: "US",
43+
});
44+
});
45+
46+
test("CLOUDFLARE_CF_FETCH_ENABLED=FALSE (uppercase) disables cf fetching", async ({
47+
expect,
48+
}) => {
49+
process.env.CLOUDFLARE_CF_FETCH_ENABLED = "FALSE";
50+
51+
const mf = new Miniflare({
52+
script: "",
53+
modules: true,
54+
});
55+
useDispose(mf);
56+
57+
const cf = await mf.getCf();
58+
// Should return fallback cf object when fetching is disabled
59+
expect(cf).toMatchObject({
60+
colo: "DFW",
61+
country: "US",
62+
});
63+
});
64+
65+
test("explicit cf option takes precedence over CLOUDFLARE_CF_FETCH_ENABLED", async ({
66+
expect,
67+
}) => {
68+
process.env.CLOUDFLARE_CF_FETCH_ENABLED = "false";
69+
70+
const mf = new Miniflare({
71+
script: "",
72+
modules: true,
73+
cf: { colo: "CUSTOM", country: "GB" },
74+
});
75+
useDispose(mf);
76+
77+
const cf = await mf.getCf();
78+
// Explicit cf option should take precedence
79+
expect(cf).toEqual({ colo: "CUSTOM", country: "GB" });
80+
});
81+
82+
test("explicit cf option takes precedence over CLOUDFLARE_CF_FETCH_PATH", async ({
83+
expect,
84+
}) => {
85+
process.env.CLOUDFLARE_CF_FETCH_PATH = "/some/custom/path.json";
86+
87+
const mf = new Miniflare({
88+
script: "",
89+
modules: true,
90+
cf: { colo: "CUSTOM", country: "GB" },
91+
});
92+
useDispose(mf);
93+
94+
const cf = await mf.getCf();
95+
// Explicit cf option should take precedence
96+
expect(cf).toEqual({ colo: "CUSTOM", country: "GB" });
97+
});
98+
99+
test("CLOUDFLARE_CF_FETCH_ENABLED takes precedence over CLOUDFLARE_CF_FETCH_PATH when disabled", async ({
100+
expect,
101+
}) => {
102+
process.env.CLOUDFLARE_CF_FETCH_ENABLED = "false";
103+
process.env.CLOUDFLARE_CF_FETCH_PATH = "/some/custom/path.json";
104+
105+
const mf = new Miniflare({
106+
script: "",
107+
modules: true,
108+
});
109+
useDispose(mf);
110+
111+
const cf = await mf.getCf();
112+
// Should return fallback cf object when fetching is disabled, ignoring the path
113+
expect(cf).toMatchObject({
114+
colo: "DFW",
115+
country: "US",
116+
});
117+
});
118+
119+
test("empty CLOUDFLARE_CF_FETCH_PATH uses default path", async ({
120+
expect,
121+
}) => {
122+
// Setting to empty string should be treated as unset (use default behavior)
123+
process.env.CLOUDFLARE_CF_FETCH_PATH = "";
124+
125+
const mf = new Miniflare({
126+
script: "",
127+
modules: true,
128+
});
129+
useDispose(mf);
130+
131+
const cf = await mf.getCf();
132+
// Should return fallback cf object (default test behavior)
133+
// This verifies empty string doesn't cause issues and uses default path
134+
expect(cf).toMatchObject({
135+
colo: "DFW",
136+
country: "US",
137+
});
138+
});
139+
});

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ type VariableNames =
2424

2525
// ## Development & Local Testing
2626

27+
/** Controls whether to fetch the cf.json file. Set to "false" or "0" to disable fetching and use fallback data. Defaults to "true". */
28+
| "CLOUDFLARE_CF_FETCH_ENABLED"
29+
/** Custom path for caching the cf.json file. Overrides the default node_modules/.mf/cf.json location. */
30+
| "CLOUDFLARE_CF_FETCH_PATH"
2731
/** Local database connection strings for Hyperdrive development. The * should be replaced with the Hyperdrive binding name in the Worker. */
2832
| `CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_${string}`
2933
/** Suppress Hyperdrive-related warnings during development. */

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,3 +346,40 @@ export const getLocalExplorerEnabledFromEnv =
346346
variableName: "X_LOCAL_EXPLORER",
347347
defaultValue: false,
348348
});
349+
350+
/**
351+
* `CLOUDFLARE_CF_FETCH_ENABLED` controls whether Miniflare fetches the `cf.json` file
352+
* containing request.cf properties from workers.cloudflare.com.
353+
*
354+
* - If set to "false", disables fetching and uses fallback data (no files created)
355+
* - If set to "true" or not set, uses the default behavior (fetches and caches cf.json)
356+
*
357+
* This is particularly useful for non-JavaScript projects that don't want
358+
* a node_modules directory created automatically.
359+
*
360+
* Example:
361+
* ```sh
362+
* # Disable cf fetching entirely
363+
* CLOUDFLARE_CF_FETCH_ENABLED=false npx wrangler dev
364+
* ```
365+
*/
366+
export const getCfFetchEnabledFromEnv = getBooleanEnvironmentVariableFactory({
367+
variableName: "CLOUDFLARE_CF_FETCH_ENABLED",
368+
defaultValue: true,
369+
});
370+
371+
/**
372+
* `CLOUDFLARE_CF_FETCH_PATH` specifies a custom path for caching the cf.json file.
373+
*
374+
* - If set, uses the specified path instead of the default node_modules/.mf/cf.json
375+
* - If not set, uses the default location (node_modules/.mf/cf.json)
376+
*
377+
* Example:
378+
* ```sh
379+
* # Use a custom cache location
380+
* CLOUDFLARE_CF_FETCH_PATH=/tmp/cf-cache.json npx wrangler dev
381+
* ```
382+
*/
383+
export const getCfFetchPathFromEnv = getEnvironmentVariableFactory({
384+
variableName: "CLOUDFLARE_CF_FETCH_PATH",
385+
});

0 commit comments

Comments
 (0)