Skip to content
16 changes: 16 additions & 0 deletions .changeset/strong-otters-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"wrangler": minor
---

feature: add a `cf` field to the `getBindingsProxy` result

Add a new `cf` field to the `getBindingsProxy` result that people can use to mock the production
`cf` (`IncomingRequestCfProperties`) object.

Example:

```ts
const { cf } = await getBindingsProxy();

console.log(`country = ${cf.country}; colo = ${cf.colo}`); // logs 'country = GB ; colo = LHR'
```
43 changes: 43 additions & 0 deletions fixtures/get-bindings-proxy/tests/get-bindings-proxy.cf.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import { getBindingsProxy } from "./shared";

describe("getBindingsProxy - cf", () => {
it("should provide mock data", async () => {
const { cf, dispose } = await getBindingsProxy();
try {
expect(cf).toMatchObject({
colo: "DFW",
city: "Austin",
regionCode: "TX",
});
} finally {
await dispose();
}
});

it("should match the production runtime cf object", async () => {
const { cf, dispose } = await getBindingsProxy();
try {
expect(cf.constructor.name).toBe("Object");

expect(() => {
cf.city = "test city";
}).toThrowError(
"Cannot assign to read only property 'city' of object '#<Object>'"
);
expect(cf.city).not.toBe("test city");

expect(() => {
cf.newField = "test new field";
}).toThrowError("Cannot add property newField, object is not extensible");
expect("newField" in cf).toBe(false);

expect(cf.botManagement).toMatchObject({
score: 99,
});
expect(Object.isFrozen(cf.botManagement)).toBe(true);
} finally {
await dispose();
}
});
});
20 changes: 20 additions & 0 deletions packages/wrangler/src/api/integrations/bindings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ export type BindingsProxy<Bindings = Record<string, unknown>> = {
* Object containing the various proxies
*/
bindings: Bindings;
/**
* Mock of the context object that Workers received in their request handler, all the object's methods are no-op
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cf: Record<string, any>;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PS: I wanted to use IncomingRequestCfProperties here but I've been having issues with the @microsoft/api-extractor package (it seems not to cope with workers-types for some reason 😕)

I would go with this simple type for now and look into improving the types later on (also as I want to get proper types for #4792 anyways)

But I don't mind spending more time investigating the api-extractor problem for this PR if that's preferred 🙂

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about we make this Record<string, string> initially and later replace it with IncomingRequestCfProperties? That way you'll be widening the type which is a non-breaking change.

If you go with any, you'll be forced to narrow the type in the future which is breaking.

/**
* Mock of the context object that Workers received in their request handler, all the object's methods are no-op
*/
Expand Down Expand Up @@ -88,11 +93,15 @@ export async function getBindingsProxy<Bindings = Record<string, unknown>>(

const vars = getVarsForDev(rawConfig, env);

const cf = await mf.getCf();
deepFreeze(cf);

return {
bindings: {
...vars,
...bindings,
},
cf,
ctx: new ExecutionContext(),
caches: new CacheStorage(),
dispose: () => mf.dispose(),
Expand Down Expand Up @@ -177,3 +186,14 @@ function getMiniflarePersistOptions(
d1Persist: `${persistPath}/d1`,
};
}

function deepFreeze<T extends Record<string | number | symbol, unknown>>(
obj: T
): void {
Object.freeze(obj);
Object.entries(obj).forEach(([, prop]) => {
if (prop !== null && typeof prop === "object" && !Object.isFrozen(prop)) {
deepFreeze(prop as Record<string | number | symbol, unknown>);
}
});
}