diff --git a/.changeset/soft-tigers-beg.md b/.changeset/soft-tigers-beg.md new file mode 100644 index 000000000000..b69c1a521751 --- /dev/null +++ b/.changeset/soft-tigers-beg.md @@ -0,0 +1,16 @@ +--- +"wrangler": minor +--- + +feature: add a `ctx` field to the `getBindingsProxy` result + +Add a new `ctx` filed to the `getBindingsProxy` result that people can use to mock the production +`ExecutionContext` object. + +Example: + +```ts +const { ctx } = await getBindingsProxy(); +// ... +ctx.waitUntil(myPromise); +``` diff --git a/fixtures/get-bindings-proxy/tests/get-bindings-proxy.test.ts b/fixtures/get-bindings-proxy/tests/get-bindings-proxy.bindings.test.ts similarity index 89% rename from fixtures/get-bindings-proxy/tests/get-bindings-proxy.test.ts rename to fixtures/get-bindings-proxy/tests/get-bindings-proxy.bindings.test.ts index 560840f2323d..e9c97442b039 100644 --- a/fixtures/get-bindings-proxy/tests/get-bindings-proxy.test.ts +++ b/fixtures/get-bindings-proxy/tests/get-bindings-proxy.bindings.test.ts @@ -6,7 +6,6 @@ import { Fetcher, R2Bucket, } from "@cloudflare/workers-types"; -import { Request, Response } from "undici"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { getBindingsProxy as originalGetBindingsProxy, @@ -42,7 +41,7 @@ function getBindingsProxy( }); } -describe("getBindingsProxy", () => { +describe("getBindingsProxy - bindings", () => { let devWorkers: UnstableDevWorker[]; beforeAll(async () => { @@ -228,25 +227,6 @@ describe("getBindingsProxy", () => { await dispose(); } }); - - describe("caches", () => { - (["default", "named"] as const).forEach((cacheType) => - it(`correctly obtains a no-op ${cacheType} cache`, async () => { - const { caches, dispose } = await getBindingsProxy({ - configPath: wranglerTomlFilePath, - }); - try { - const cache = - cacheType === "default" - ? caches.default - : await caches.open("my-cache"); - testNoOpCache(cache); - } finally { - await dispose(); - } - }) - ); - }); }); /** @@ -283,17 +263,3 @@ async function testDoBinding( const doRespText = await doResp.text(); expect(doRespText).toBe(expectedResponse); } - -async function testNoOpCache( - cache: Awaited>["caches"]["default"] -) { - let match = await cache.match("http://0.0.0.0/test"); - expect(match).toBeUndefined(); - - const req = new Request("http://0.0.0.0/test"); - await cache.put(req, new Response("test")); - const resp = await cache.match(req); - expect(resp).toBeUndefined(); - const deleted = await cache.delete(req); - expect(deleted).toBe(false); -} diff --git a/fixtures/get-bindings-proxy/tests/get-bindings-proxy.caches.test.ts b/fixtures/get-bindings-proxy/tests/get-bindings-proxy.caches.test.ts new file mode 100644 index 000000000000..8df2a3ac65aa --- /dev/null +++ b/fixtures/get-bindings-proxy/tests/get-bindings-proxy.caches.test.ts @@ -0,0 +1,34 @@ +import { Request, Response } from "undici"; +import { describe, expect, it } from "vitest"; +import { getBindingsProxy } from "./shared"; + +describe("getBindingsProxy - caches", () => { + (["default", "named"] as const).forEach((cacheType) => + it(`correctly obtains a no-op ${cacheType} cache`, async () => { + const { caches, dispose } = await getBindingsProxy(); + try { + const cache = + cacheType === "default" + ? caches.default + : await caches.open("my-cache"); + testNoOpCache(cache); + } finally { + await dispose(); + } + }) + ); +}); + +async function testNoOpCache( + cache: Awaited>["caches"]["default"] +) { + let match = await cache.match("http://0.0.0.0/test"); + expect(match).toBeUndefined(); + + const req = new Request("http://0.0.0.0/test"); + await cache.put(req, new Response("test")); + const resp = await cache.match(req); + expect(resp).toBeUndefined(); + const deleted = await cache.delete(req); + expect(deleted).toBe(false); +} diff --git a/fixtures/get-bindings-proxy/tests/get-bindings-proxy.ctx.test.ts b/fixtures/get-bindings-proxy/tests/get-bindings-proxy.ctx.test.ts new file mode 100644 index 000000000000..61f2d53a4d66 --- /dev/null +++ b/fixtures/get-bindings-proxy/tests/get-bindings-proxy.ctx.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { getBindingsProxy } from "./shared"; + +describe("getBindingsProxy - ctx", () => { + it("should provide a no-op waitUntil method", async () => { + const { ctx, dispose } = await getBindingsProxy(); + try { + let value = 4; + ctx.waitUntil( + new Promise((resolve) => { + value++; + resolve(value); + }) + ); + expect(value).toBe(5); + } finally { + await dispose(); + } + }); + + it("should provide a no-op passThroughOnException method", async () => { + const { ctx, dispose } = await getBindingsProxy(); + try { + expect(ctx.passThroughOnException()).toBe(undefined); + } finally { + await dispose(); + } + }); + + it("should match the production runtime ctx object", async () => { + const { ctx, dispose } = await getBindingsProxy(); + try { + expect(ctx.constructor.name).toBe("ExecutionContext"); + expect(typeof ctx.waitUntil).toBe("function"); + expect(typeof ctx.passThroughOnException).toBe("function"); + + ctx.waitUntil = ((str: string) => `- ${str} -`) as any; + expect(ctx.waitUntil("waitUntil can be overridden" as any)).toBe( + "- waitUntil can be overridden -" + ); + + ctx.passThroughOnException = ((str: string) => `_ ${str} _`) as any; + expect( + (ctx.passThroughOnException as any)( + "passThroughOnException can be overridden" + ) + ).toBe("_ passThroughOnException can be overridden _"); + + (ctx as any).text = "the ExecutionContext can be extended"; + expect((ctx as any).text).toBe("the ExecutionContext can be extended"); + } finally { + await dispose(); + } + }); +}); diff --git a/fixtures/get-bindings-proxy/tests/shared.ts b/fixtures/get-bindings-proxy/tests/shared.ts new file mode 100644 index 000000000000..f54262466cac --- /dev/null +++ b/fixtures/get-bindings-proxy/tests/shared.ts @@ -0,0 +1,13 @@ +import { getBindingsProxy as originalGetBindingsProxy } from "wrangler"; +import type { GetBindingsProxyOptions } from "wrangler"; + +// Here we wrap the actual original getBindingsProxy function and disable its persistance, this is to make sure +// that we don't implement any persistance during these tests (which would add unnecessary extra complexity) +export function getBindingsProxy( + options: Omit = {} +): ReturnType> { + return originalGetBindingsProxy({ + ...options, + persist: false, + }); +} diff --git a/packages/wrangler/src/api/integrations/bindings/executionContext.ts b/packages/wrangler/src/api/integrations/bindings/executionContext.ts new file mode 100644 index 000000000000..e940c026a8f2 --- /dev/null +++ b/packages/wrangler/src/api/integrations/bindings/executionContext.ts @@ -0,0 +1,5 @@ +export class ExecutionContext { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, unused-imports/no-unused-vars + waitUntil(promise: Promise): void {} + passThroughOnException(): void {} +} diff --git a/packages/wrangler/src/api/integrations/bindings/index.ts b/packages/wrangler/src/api/integrations/bindings/index.ts index db5b6c8e01d3..487dae15f810 100644 --- a/packages/wrangler/src/api/integrations/bindings/index.ts +++ b/packages/wrangler/src/api/integrations/bindings/index.ts @@ -5,6 +5,7 @@ import { getBoundRegisteredWorkers } from "../../../dev-registry"; import { getVarsForDev } from "../../../dev/dev-vars"; import { buildMiniflareBindingOptions } from "../../../dev/miniflare"; import { CacheStorage } from "./caches"; +import { ExecutionContext } from "./executionContext"; import { getServiceBindings } from "./services"; import type { Config } from "../../../config"; import type { MiniflareOptions } from "miniflare"; @@ -40,6 +41,10 @@ export type BindingsProxy> = { * 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 + */ + ctx: ExecutionContext; /** * Caches object emulating the Workers Cache runtime API */ @@ -88,6 +93,7 @@ export async function getBindingsProxy>( ...vars, ...bindings, }, + ctx: new ExecutionContext(), caches: new CacheStorage(), dispose: () => mf.dispose(), };