From cd03d1d3fa6e733faa42e5abb92f37637503b327 Mon Sep 17 00:00:00 2001 From: Greg Brimble Date: Wed, 3 Apr 2024 17:42:35 -0400 Subject: [PATCH] Add support for named entrypoints (#5215) * feature: support named entrypoints in `wrangler (pages) deploy` * chore: exclude `miniflare` from monorepo's Prettier config * fix: allow `script`s without `scriptPath`s to import built-ins * feature: service binding named entrypoints for single-instance JSRPC * feature: direct socket named entrypoints for multi-instance JSRPC * refactor: switch middleware to `external` services for dev registry * feature: support binding to named entrypoints in `wrangler dev` * feature: add middleware support to `WorkerEntrypoint`s * feature: add API for starting isolated dev registry * fix: ensure `url` and `cf` blob preserved across service bindings * test: add tests for named entrypoints and RPC * fixup! test: add tests for named entrypoints and RPC Update error messages * fix: improve error message for cross-session Durable Object RPC * chore: move service binding assignment before Durable Objects * fix: improve error message for RPC on not found service * test: update deploy snapshot * Add test for binding to own named entrypoint * fix: allow named entrypoints to current worker * Simplify cross-Worker Durable Object RPC error message * Bump @cloudflare/workers-types@4.20240402.0 --------- Co-authored-by: bcoll --- .changeset/afraid-jeans-tap.md | 7 + .changeset/brown-cycles-live.md | 9 + .changeset/eight-seahorses-serve.md | 44 + .changeset/lazy-papayas-knock.md | 7 + .changeset/tidy-fans-speak.md | 48 + fixtures/additional-modules/package.json | 2 +- fixtures/entrypoints-rpc-tests/package.json | 15 + .../tests/entrypoints.spec.ts | 928 ++++++++++++++++++ fixtures/entrypoints-rpc-tests/tsconfig.json | 4 + .../entrypoints-rpc-tests/vitest.config.ts | 9 + fixtures/entrypoints-rpc-tests/wrangler.toml | 2 + .../external-durable-objects-app/package.json | 2 +- .../package.json | 2 +- fixtures/get-bindings-proxy/package.json | 2 +- fixtures/get-platform-proxy/package.json | 2 +- fixtures/local-mode-tests/package.json | 2 +- fixtures/node-app-pages/package.json | 2 +- .../pages-dev-proxy-with-script/package.json | 2 +- fixtures/pages-functions-app/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- fixtures/pages-simple-assets/package.json | 2 +- .../shared/src/run-wrangler-long-lived.ts | 41 +- fixtures/worker-ts/package.json | 2 +- package.json | 2 +- packages/create-cloudflare/package.json | 2 +- .../package.json | 2 +- packages/format-errors/package.json | 2 +- packages/kv-asset-handler/package.json | 2 +- packages/miniflare/.prettierignore | 3 + packages/miniflare/.prettierrc | 6 + packages/miniflare/README.md | 11 +- packages/miniflare/src/index.ts | 104 +- packages/miniflare/src/plugins/core/index.ts | 28 +- .../miniflare/src/plugins/core/modules.ts | 18 +- .../miniflare/src/plugins/core/services.ts | 25 +- .../miniflare/src/plugins/shared/constants.ts | 8 +- .../src/runtime/config/workerd.capnp | 26 +- .../src/runtime/config/workerd.capnp.d.ts | 41 +- .../src/runtime/config/workerd.capnp.js | 43 +- .../miniflare/src/runtime/config/workerd.ts | 12 + packages/miniflare/test/index.spec.ts | 183 +++- packages/pages-shared/package.json | 2 +- .../playground-preview-worker/package.json | 2 +- packages/prerelease-registry/package.json | 2 +- packages/quick-edit-extension/package.json | 2 +- packages/workers.new/package.json | 2 +- packages/wrangler/package.json | 5 +- .../src/__tests__/configuration.test.ts | 46 +- .../wrangler/src/__tests__/deploy.test.ts | 43 + packages/wrangler/src/__tests__/init.test.ts | 3 + .../wrangler/src/__tests__/middleware.test.ts | 145 ++- packages/wrangler/src/api/dev.ts | 1 + .../src/api/integrations/platform/index.ts | 3 + .../src/api/startDevWorker/ProxyController.ts | 8 +- .../wrangler/src/api/startDevWorker/events.ts | 2 + packages/wrangler/src/cli.ts | 1 + packages/wrangler/src/config/environment.ts | 2 + packages/wrangler/src/config/index.ts | 4 +- packages/wrangler/src/config/validation.ts | 10 +- packages/wrangler/src/deploy/deploy.ts | 5 - .../src/deployment-bundle/apply-middleware.ts | 47 +- .../wrangler/src/deployment-bundle/bundle.ts | 32 +- .../create-worker-upload-form.ts | 27 +- .../wrangler/src/deployment-bundle/worker.ts | 1 + packages/wrangler/src/dev-registry.ts | 91 +- packages/wrangler/src/dev.tsx | 3 +- packages/wrangler/src/dev/dev.tsx | 5 +- packages/wrangler/src/dev/local.tsx | 14 +- packages/wrangler/src/dev/miniflare.ts | 317 +++++- packages/wrangler/src/dev/remote.tsx | 2 + packages/wrangler/src/dev/start-server.ts | 14 +- packages/wrangler/src/dev/use-esbuild.ts | 2 - packages/wrangler/src/init.ts | 1 + packages/wrangler/src/pages/dev.ts | 19 +- packages/wrangler/src/versions/upload.ts | 5 - packages/wrangler/templates/facade.d.ts | 12 +- .../templates/middleware/loader-modules.ts | 189 ++-- .../middleware/middleware-d1-beta.d.ts | 3 - .../middleware-ensure-req-body-drained.ts | 1 - .../middleware-miniflare3-json-error.ts | 1 - .../middleware-multiworker-dev.d.ts | 4 - .../middleware/middleware-multiworker-dev.ts | 68 -- packages/wrangler/turbo.json | 3 +- pnpm-lock.yaml | 135 +-- 85 files changed, 2287 insertions(+), 655 deletions(-) create mode 100644 .changeset/afraid-jeans-tap.md create mode 100644 .changeset/brown-cycles-live.md create mode 100644 .changeset/eight-seahorses-serve.md create mode 100644 .changeset/lazy-papayas-knock.md create mode 100644 .changeset/tidy-fans-speak.md create mode 100644 fixtures/entrypoints-rpc-tests/package.json create mode 100644 fixtures/entrypoints-rpc-tests/tests/entrypoints.spec.ts create mode 100644 fixtures/entrypoints-rpc-tests/tsconfig.json create mode 100644 fixtures/entrypoints-rpc-tests/vitest.config.ts create mode 100644 fixtures/entrypoints-rpc-tests/wrangler.toml create mode 100644 packages/miniflare/.prettierignore create mode 100644 packages/miniflare/.prettierrc delete mode 100644 packages/wrangler/templates/middleware/middleware-d1-beta.d.ts delete mode 100644 packages/wrangler/templates/middleware/middleware-multiworker-dev.d.ts delete mode 100644 packages/wrangler/templates/middleware/middleware-multiworker-dev.ts diff --git a/.changeset/afraid-jeans-tap.md b/.changeset/afraid-jeans-tap.md new file mode 100644 index 000000000000..5db4e8a6fe1f --- /dev/null +++ b/.changeset/afraid-jeans-tap.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +fix: ensure request `url` and `cf` properties preserved across service bindings + +Previously, Wrangler could rewrite `url` and `cf` properties when sending requests via service bindings or Durable Object stubs. To match production behaviour, this change ensures these properties are preserved. diff --git a/.changeset/brown-cycles-live.md b/.changeset/brown-cycles-live.md new file mode 100644 index 000000000000..05a5acc32fdb --- /dev/null +++ b/.changeset/brown-cycles-live.md @@ -0,0 +1,9 @@ +--- +"miniflare": minor +--- + +feature: customisable unsafe direct sockets entrypoints + +Previously, Miniflare provided experimental `unsafeDirectHost` and `unsafeDirectPort` options for starting an HTTP server that pointed directly to a specific Worker. This change replaces these options with a single `unsafeDirectSockets` option that accepts an array of socket objects of the form `{ host?: string, port?: number, entrypoint?: string, proxy?: boolean }`. `host` defaults to `127.0.0.1`, `port` defaults to `0`, `entrypoint` defaults to `default`, and `proxy` defaults to `false`. This allows you to start HTTP servers for specific entrypoints of specific Workers. `proxy` controls the [`Style`](https://github.com/cloudflare/workerd/blob/af35f1e7b0f166ec4ca93a8bf7daeacda029f11d/src/workerd/server/workerd.capnp#L780-L789) of the socket. + +Note these sockets set the `capnpConnectHost` `workerd` option to `"miniflare-unsafe-internal-capnp-connect"`. `external` `serviceBindings` will set their `capnpConnectHost` option to the same value allowing RPC over multiple `Miniflare` instances. Refer to https://github.com/cloudflare/workerd/pull/1757 for more information. diff --git a/.changeset/eight-seahorses-serve.md b/.changeset/eight-seahorses-serve.md new file mode 100644 index 000000000000..7021461fadc2 --- /dev/null +++ b/.changeset/eight-seahorses-serve.md @@ -0,0 +1,44 @@ +--- +"wrangler": minor +--- + +feature: support named entrypoints in service bindings + +This change allows service bindings to bind to a named export of another Worker. As an example, consider the following Worker named `bound`: + +```ts +import { WorkerEntrypoint } from "cloudflare:workers"; + +export class EntrypointA extends WorkerEntrypoint { + fetch(request) { + return new Response("Hello from entrypoint A!"); + } +} + +export const entrypointB: ExportedHandler = { + fetch(request, env, ctx) { + return new Response("Hello from entrypoint B!"); + } +}; + +export default { + fetch(request, env, ctx) { + return new Response("Hello from the default entrypoint!"); + } +}; +``` + +Up until now, you could only bind to the `default` entrypoint. With this change, you can bind to `EntrypointA` or `entrypointB` too using the new `entrypoint` option: + +```toml +[[services]] +binding = "SERVICE" +service = "bound" +entrypoint = "EntrypointA" +``` + +To bind to named entrypoints with `wrangler pages dev`, use the `#` character: + +```shell +$ wrangler pages dev --service=SERVICE=bound#EntrypointA +``` diff --git a/.changeset/lazy-papayas-knock.md b/.changeset/lazy-papayas-knock.md new file mode 100644 index 000000000000..98374c04e438 --- /dev/null +++ b/.changeset/lazy-papayas-knock.md @@ -0,0 +1,7 @@ +--- +"miniflare": patch +--- + +fix: allow `script`s without `scriptPath`s to import built-in modules + +Previously, if a string `script` option was specified with `modules: true` but without a corresponding `scriptPath`, all `import`s were forbidden. This change relaxes that restriction to allow imports of built-in `node:*`, `cloudflare:*` and `workerd:*` modules without a `scriptPath`. diff --git a/.changeset/tidy-fans-speak.md b/.changeset/tidy-fans-speak.md new file mode 100644 index 000000000000..eaba8fd8d09c --- /dev/null +++ b/.changeset/tidy-fans-speak.md @@ -0,0 +1,48 @@ +--- +"miniflare": minor +--- + +feature: support named entrypoints for `serviceBindings` + +This change allows service bindings to bind to a named export of another Worker using designators of the form `{ name: string | typeof kCurrentWorker, entrypoint?: string }`. Previously, you could only bind to the `default` entrypoint. With this change, you can bind to any exported entrypoint. + +```ts +import { kCurrentWorker, Miniflare } from "miniflare"; + +const mf = new Miniflare({ + workers: [ + { + name: "a", + serviceBindings: { + A_RPC_SERVICE: { name: kCurrentWorker, entrypoint: "RpcEntrypoint" }, + A_NAMED_SERVICE: { name: "a", entrypoint: "namedEntrypoint" }, + B_NAMED_SERVICE: { name: "b", entrypoint: "anotherNamedEntrypoint" }, + }, + compatibilityFlags: ["rpc"], + modules: true, + script: ` + import { WorkerEntrypoint } from "cloudflare:workers"; + + export class RpcEntrypoint extends WorkerEntrypoint { + ping() { return "a:rpc:pong"; } + } + + export const namedEntrypoint = { + fetch(request, env, ctx) { return new Response("a:named:pong"); } + }; + + ... + `, + }, + { + name: "b", + modules: true, + script: ` + export const anotherNamedEntrypoint = { + fetch(request, env, ctx) { return new Response("b:named:pong"); } + }; + `, + }, + ], +}); +``` diff --git a/fixtures/additional-modules/package.json b/fixtures/additional-modules/package.json index a3d537a399f3..47cf50748083 100644 --- a/fixtures/additional-modules/package.json +++ b/fixtures/additional-modules/package.json @@ -12,7 +12,7 @@ }, "devDependencies": { "@cloudflare/workers-tsconfig": "workspace:*", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "undici": "^5.28.3", "wrangler": "workspace:*" } diff --git a/fixtures/entrypoints-rpc-tests/package.json b/fixtures/entrypoints-rpc-tests/package.json new file mode 100644 index 000000000000..52bd37b072e5 --- /dev/null +++ b/fixtures/entrypoints-rpc-tests/package.json @@ -0,0 +1,15 @@ +{ + "name": "entrypoints-rpc-tests", + "private": true, + "scripts": { + "test": "vitest run", + "test:ci": "vitest run", + "test:watch": "vitest" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "wrangler": "workspace:*", + "ts-dedent": "^2.2.0", + "undici": "^5.28.3" + } +} diff --git a/fixtures/entrypoints-rpc-tests/tests/entrypoints.spec.ts b/fixtures/entrypoints-rpc-tests/tests/entrypoints.spec.ts new file mode 100644 index 000000000000..0c78ee5009a6 --- /dev/null +++ b/fixtures/entrypoints-rpc-tests/tests/entrypoints.spec.ts @@ -0,0 +1,928 @@ +import assert from "node:assert"; +import fs, { mkdir, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import dedent from "ts-dedent"; +import { Agent, fetch, setGlobalDispatcher } from "undici"; +import { test as baseTest, expect, vi } from "vitest"; +import { unstable_startWorkerRegistryServer } from "wrangler"; +import { + runWranglerDev, + runWranglerPagesDev, +} from "../../shared/src/run-wrangler-long-lived"; + +const timeoutAgent = new Agent({ + connectTimeout: 500, + bodyTimeout: 500, + headersTimeout: 500, +}); +setGlobalDispatcher(timeoutAgent); + +const tmpPathBase = path.join(os.tmpdir(), "wrangler-tests"); + +type WranglerDevSession = Awaited>; +type StartDevSession = ( + files: Record, + flags?: string[], + pagesPublicPath?: string +) => Promise<{ url: URL; session: WranglerDevSession }>; + +export async function seed(root: string, files: Record) { + for (const [name, contents] of Object.entries(files)) { + const filePath = path.resolve(root, name); + await mkdir(path.dirname(filePath), { recursive: true }); + await writeFile(filePath, contents); + } +} + +function waitFor(callback: Parameters>[0]) { + // The default timeout of `vi.waitFor()` is only 1s, which is a little + // short for some of these tests, especially on Windows. + return vi.waitFor(callback, { timeout: 5_000, interval: 250 }); +} + +const test = baseTest.extend<{ + tmpPath: string; + isolatedDevRegistryPort: number; + dev: StartDevSession; +}>({ + // Fixture for creating a temporary directory + async tmpPath({}, use) { + const tmpPath = await fs.realpath(await fs.mkdtemp(tmpPathBase)); + await use(tmpPath); + await fs.rm(tmpPath, { recursive: true, maxRetries: 10 }); + }, + // Fixture for starting an isolated dev registry server on a random port + async isolatedDevRegistryPort({}, use) { + // Start a standalone dev registry server for each test + const result = await unstable_startWorkerRegistryServer(0); + const address = result.server.address(); + assert(typeof address === "object" && address !== null); + await use(address.port); + await result.terminator.terminate(); + }, + // Fixture for starting a worker in a temporary directory, using the test's + // isolated dev registry + async dev({ tmpPath, isolatedDevRegistryPort }, use) { + const workerTmpPathBase = path.join(tmpPath, "worker-"); + const cleanups: (() => Promise)[] = []; + + const fn: StartDevSession = async (files, flags, pagesPublicPath) => { + const workerPath = await fs.mkdtemp(workerTmpPathBase); + await seed(workerPath, files); + + let session: WranglerDevSession; + if (pagesPublicPath !== undefined) { + session = await runWranglerPagesDev( + workerPath, + pagesPublicPath, + ["--port=0", "--inspector-port=0", ...(flags ?? [])], + { WRANGLER_WORKER_REGISTRY_PORT: String(isolatedDevRegistryPort) } + ); + } else { + session = await runWranglerDev( + workerPath, + ["--port=0", "--inspector-port=0", ...(flags ?? [])], + { WRANGLER_WORKER_REGISTRY_PORT: String(isolatedDevRegistryPort) } + ); + } + + cleanups.push(session.stop); + // noinspection HttpUrlsUsage + const url = new URL(`http://${session.ip}:${session.port}`); + return { url, session }; + }; + + await use(fn); + + await Promise.allSettled(cleanups.map((fn) => fn())); + }, +}); + +test("should support binding to the same worker", async ({ dev }) => { + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [[services]] + binding = "SERVICE" + service = "entry" + `, + "index.ts": dedent` + export default { + fetch(request, env, ctx) { + const { pathname } = new URL(request.url); + + if (pathname === "/loopback") { + return new Response(\`\${request.method} \${request.url} \${JSON.stringify(request.cf)}\`); + } + + return env.SERVICE.fetch("https://placeholder:9999/loopback", { + method: "POST", + cf: { thing: true }, + }); + } + } + `, + }); + + const response = await fetch(url); + // Check protocol, host, and cf preserved + expect(await response.text()).toBe( + 'POST https://placeholder:9999/loopback {"thing":true}' + ); +}); + +test("should support default ExportedHandler entrypoints", async ({ dev }) => { + await dev({ + "wrangler.toml": dedent` + name = "bound" + main = "index.ts" + `, + "index.ts": dedent` + export default { + fetch(request, env, ctx) { + return new Response(\`\${request.method} \${request.url} \${JSON.stringify(request.cf)}\`); + } + }; + `, + }); + + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [[services]] + binding = "SERVICE" + service = "bound" + `, + "index.ts": dedent` + export default { + fetch(request, env, ctx) { + return env.SERVICE.fetch("https://placeholder:9999/", { + method: "POST", + cf: { thing: true }, + }); + } + } + `, + }); + + await waitFor(async () => { + const response = await fetch(url); + const text = await response.text(); + // Check protocol, host, and cf preserved + expect(text).toBe('POST https://placeholder:9999/ {"thing":true}'); + }); +}); + +test("should support default WorkerEntrypoint entrypoints", async ({ dev }) => { + await dev({ + "wrangler.toml": dedent` + name = "bound" + main = "index.ts" + `, + "index.ts": dedent` + import { WorkerEntrypoint } from "cloudflare:workers"; + // Check middleware is transparent to RPC + export default class ThingEntrypoint extends WorkerEntrypoint { + fetch(request) { + return new Response(\`\${request.method} \${request.url} \${JSON.stringify(request.cf)}\`); + } + ping() { + return "pong"; + } + }; + `, + }); + + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + compatibility_flags = ["rpc"] + + [[services]] + binding = "SERVICE" + service = "bound" + `, + "index.ts": dedent` + export default { + async fetch(request, env, ctx) { + const response = await env.SERVICE.fetch("https://placeholder:9999/", { + method: "POST", + cf: { thing: true }, + }); + const text = await response.text(); + const pong = await env.SERVICE.ping(); + return new Response(\`\${text} \${pong}\`); + } + } + `, + }); + + await waitFor(async () => { + const response = await fetch(url); + const text = await response.text(); + // Check protocol, host, and cf preserved + expect(text).toBe('POST https://placeholder:9999/ {"thing":true} pong'); + }); +}); + +test("should support middleware with default WorkerEntrypoint entrypoints", async ({ + dev, +}) => { + const files: Record = { + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [[services]] + binding = "SERVICE" + service = "entry" + `, + "index.ts": dedent` + import { WorkerEntrypoint } from "cloudflare:workers"; + let lastController; + export default class TestEntrypoint extends WorkerEntrypoint { + fetch(request) { + const { pathname } = new URL(request.url); + if (pathname === "/throw") throw new Error("Oops!"); + if (pathname === "/controller") return new Response(lastController.cron); + return new Response(\`\${request.method} \${new URL(request.url).pathname}\`); + } + scheduled(controller) { + lastController = controller; + } + } + `, + }; + const { url } = await dev(files, ["--test-scheduled"]); + + let response = await fetch(url); + expect(await response.text()).toBe("GET /"); + + // Check other events can be dispatched + response = await fetch(new URL("/__scheduled?cron=* * * * 30", url)); + expect(response.status).toBe(200); + expect(await response.text()).toBe("Ran scheduled event"); + response = await fetch(new URL("/controller", url)); + expect(response.status).toBe(200); + expect(await response.text()).toBe("* * * * 30"); + + // Check multiple middleware can be registered + response = await fetch(new URL("/throw", url)); + expect(response.status).toBe(500); + expect(response.headers.get("Content-Type")).toMatch(/text\/html/); + expect(await response.text()).toMatch("Oops!"); +}); + +test("should support named ExportedHandler entrypoints to itself", async ({ + dev, +}) => { + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [[services]] + binding = "SERVICE" + service = "entry" + entrypoint = "ThingEntrypoint" + `, + "index.ts": dedent` + import { WorkerEntrypoint } from "cloudflare:workers"; + export class ThingEntrypoint extends WorkerEntrypoint { + fetch(request) { + return new Response(\`\${request.method} \${request.url} \${JSON.stringify(request.cf)}\`); + } + ping() { + return "pong"; + } + }; + export default { + fetch(request, env, ctx) { + return env.SERVICE.fetch("https://placeholder:9999/", { + method: "POST", + cf: { thing: true }, + }); + } + } + `, + }); + + const response = await fetch(url); + // Check protocol, host, and cf preserved + expect(await response.text()).toBe( + 'POST https://placeholder:9999/ {"thing":true}' + ); +}); + +test("should support named ExportedHandler entrypoints", async ({ dev }) => { + await dev({ + "wrangler.toml": dedent` + name = "bound" + main = "index.ts" + `, + "index.ts": dedent` + export const thing = { + fetch(request, env, ctx) { + return new Response(\`\${request.method} \${request.url} \${JSON.stringify(request.cf)}\`); + } + }; + export default {}; // Required to treat as modules format worker + `, + }); + + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [[services]] + binding = "SERVICE" + service = "bound" + entrypoint = "thing" + `, + "index.ts": dedent` + export default { + fetch(request, env, ctx) { + return env.SERVICE.fetch("https://placeholder:9999/", { + method: "POST", + cf: { thing: true }, + }); + } + } + `, + }); + + await waitFor(async () => { + const response = await fetch(url); + const text = await response.text(); + // Check protocol, host, and cf preserved + expect(text).toBe('POST https://placeholder:9999/ {"thing":true}'); + }); +}); + +test("should support named WorkerEntrypoint entrypoints", async ({ dev }) => { + await dev({ + "wrangler.toml": dedent` + name = "bound" + main = "index.ts" + `, + "index.ts": dedent` + import { WorkerEntrypoint } from "cloudflare:workers"; + export class ThingEntrypoint extends WorkerEntrypoint { + fetch(request) { + return new Response(\`\${request.method} \${request.url} \${JSON.stringify(request.cf)}\`); + } + ping() { + return "pong"; + } + }; + export default {}; // Required to treat as modules format worker + `, + }); + + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + compatibility_flags = ["rpc"] + + [[services]] + binding = "SERVICE" + service = "bound" + entrypoint = "ThingEntrypoint" + `, + "index.ts": dedent` + export default { + async fetch(request, env, ctx) { + const response = await env.SERVICE.fetch("https://placeholder:9999/", { + method: "POST", + cf: { thing: true }, + }); + const text = await response.text(); + const pong = await env.SERVICE.ping(); + return new Response(\`\${text} \${pong}\`); + } + } + `, + }); + + await waitFor(async () => { + const response = await fetch(url); + const text = await response.text(); + // Check protocol, host, and cf preserved + expect(text).toBe('POST https://placeholder:9999/ {"thing":true} pong'); + }); +}); + +test("should support named entrypoints in pages dev", async ({ dev }) => { + await dev({ + "wrangler.toml": dedent` + name = "bound" + main = "index.ts" + `, + "index.ts": dedent` + import { WorkerEntrypoint } from "cloudflare:workers"; + export class ThingEntrypoint extends WorkerEntrypoint { + ping() { + return "pong"; + } + }; + export default {}; // Required to treat as modules format worker + `, + }); + + const files = { + "functions/index.ts": dedent` + export const onRequest = async ({ env }) => { + return new Response(await env.SERVICE.ping()); + }; + `, + }; + const { url } = await dev( + files, + ["--compatibility-flags=rpc", "--service=SERVICE=bound#ThingEntrypoint"], + /* pagesPublicPath */ "dist" + ); + + await waitFor(async () => { + const response = await fetch(url); + const text = await response.text(); + expect(text).toBe("pong"); + }); +}); + +test("should support co-dependent services", async ({ dev }) => { + const { url } = await dev({ + "wrangler.toml": dedent` + name = "a" + main = "index.ts" + compatibility_flags = ["rpc"] + + [[services]] + binding = "SERVICE_B" + service = "b" + entrypoint = "BEntrypoint" + `, + "index.ts": dedent` + import { WorkerEntrypoint } from "cloudflare:workers"; + export class AEntrypoint extends WorkerEntrypoint { + ping() { + return "a:pong"; + } + }; + export default { + async fetch(request, env, ctx) { + return new Response(await env.SERVICE_B.ping()); + } + }; + `, + }); + + await dev({ + "wrangler.toml": dedent` + name = "b" + main = "index.ts" + compatibility_flags = ["rpc"] + + [[services]] + binding = "SERVICE_A" + service = "a" + entrypoint = "AEntrypoint" + `, + "index.ts": dedent` + import { WorkerEntrypoint } from "cloudflare:workers"; + export class BEntrypoint extends WorkerEntrypoint { + async ping() { + const result = await this.env.SERVICE_A.ping(); + return \`b:\${result}\`; + } + }; + export default {}; // Required to treat as modules format worker + `, + }); + + await waitFor(async () => { + const response = await fetch(url); + const text = await response.text(); + expect(text).toBe("b:a:pong"); + }); +}); + +test("should support binding to Durable Object in another worker", async ({ + dev, +}) => { + // RPC isn't supported in this case yet :( + + await dev({ + "wrangler.toml": dedent` + name = "bound" + main = "index.ts" + + [durable_objects] + bindings = [ + { name = "OBJECT", class_name = "ThingObject" } + ] + `, + "index.ts": dedent` + import { DurableObject } from "cloudflare:workers"; + export class ThingObject extends DurableObject { + fetch(request) { + return new Response(\`\${request.method} \${request.url} \${JSON.stringify(request.cf)}\`); + } + get property() { + return "property:ping"; + } + method() { + return "method:ping"; + } + }; + export default {}; // Required to treat as modules format worker + `, + }); + + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + compatibility_flags = ["rpc"] + + [durable_objects] + bindings = [ + { name = "OBJECT", class_name = "ThingObject", script_name = "bound" } + ] + `, + "index.ts": dedent` + export default { + async fetch(request, env, ctx) { + const id = env.OBJECT.newUniqueId(); + const stub = env.OBJECT.get(id); + + const { pathname } = new URL(request.url); + if (pathname === "/rpc") { + const errors = []; + try { await stub.property; } catch (e) { errors.push(e); } + try { await stub.method(); } catch (e) { errors.push(e); } + return Response.json(errors.map(String)); + } + + return stub.fetch("https://placeholder:9999/", { + method: "POST", + cf: { thing: true }, + }); + } + } + `, + }); + + await waitFor(async () => { + const response = await fetch(url); + const text = await response.text(); + // Check protocol, host, and cf preserved + expect(text).toBe('POST https://placeholder:9999/ {"thing":true}'); + }); + + const rpcResponse = await fetch(new URL("/rpc", url)); + const errors = await rpcResponse.json(); + expect(errors).toMatchInlineSnapshot(` + [ + "Error: Cannot access \`ThingObject#property\` as Durable Object RPC is not yet supported between multiple \`wrangler dev\` sessions.", + "Error: Cannot access \`ThingObject#method\` as Durable Object RPC is not yet supported between multiple \`wrangler dev\` sessions.", + ] + `); +}); + +test("should support binding to Durable Object in same worker", async ({ + dev, +}) => { + // RPC is supported here though :) + + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + compatibility_flags = ["rpc"] + + [durable_objects] + bindings = [ + { name = "OBJECT", class_name = "ThingObject" } + ] + `, + "index.ts": dedent` + import { DurableObject } from "cloudflare:workers"; + export class ThingObject extends DurableObject { + ping() { + return "pong"; + } + }; + export default { + async fetch(request, env, ctx) { + const id = env.OBJECT.newUniqueId(); + const stub = env.OBJECT.get(id); + return new Response(await stub.ping()); + } + } + `, + }); + + const response = await fetch(url); + expect(await response.text()).toBe("pong"); +}); + +test("should support binding to Durable Object in same worker with explicit script_name", async ({ + dev, +}) => { + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + compatibility_flags = ["rpc"] + + [durable_objects] + bindings = [ + { name = "OBJECT", class_name = "ThingObject", script_name = "entry" } + ] + `, + "index.ts": dedent` + import { DurableObject } from "cloudflare:workers"; + export class ThingObject extends DurableObject { + ping() { + return "pong"; + } + }; + export default { + async fetch(request, env, ctx) { + const id = env.OBJECT.newUniqueId(); + const stub = env.OBJECT.get(id); + return new Response(await stub.ping()); + } + } + `, + }); + + const response = await fetch(url); + expect(await response.text()).toBe("pong"); +}); + +test("should throw if binding to named entrypoint exported by version of wrangler without entrypoints support", async ({ + dev, + isolatedDevRegistryPort, +}) => { + // Start entry worker first, so the server starts with a stubbed service not + // found binding + const { url, session } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [[services]] + binding = "SERVICE" + service = "bound" + entrypoint = "ThingEntrypoint" + `, + "index.ts": dedent` + export default { + async fetch(request, env, ctx) { + return env.SERVICE.fetch("https://placeholder:9999/"); + } + } + `, + }); + let response = await fetch(url); + expect(response.status).toBe(503); + expect(await response.text()).toBe( + '[wrangler] Couldn\'t find `wrangler dev` session for service "bound" to proxy to' + ); + + // Simulate starting up the bound worker with an old version of Wrangler + response = await fetch( + `http://127.0.0.1:${isolatedDevRegistryPort}/workers/bound`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + protocol: "http", + mode: "local", + port: 0, + host: "localhost", + durableObjects: [], + durableObjectsHost: "localhost", + durableObjectsPort: 0, + // Intentionally omitting `entrypointAddresses` + }), + } + ); + expect(response.status).toBe(200); + expect(await response.text()).toBe("null"); + + // Wait for error to be thrown + await waitFor(() => { + const output = session.getOutput(); + expect(output).toMatch( + 'The `wrangler dev` session for service "bound" does not support proxying entrypoints. Please upgrade "bound"\'s `wrangler` version.' + ); + }); +}); + +test("should throw if wrangler session doesn't export expected entrypoint", async ({ + dev, +}) => { + // Start entry worker first, so the server starts with a stubbed service not + // found binding + const { url, session } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [[services]] + binding = "SERVICE" + service = "bound" + entrypoint = "ThingEntrypoint" + `, + "index.ts": dedent` + export default { + async fetch(request, env, ctx) { + return env.SERVICE.fetch("https://placeholder:9999/"); + } + } + `, + }); + let response = await fetch(url); + expect(await response.text()).toBe( + '[wrangler] Couldn\'t find `wrangler dev` session for service "bound" to proxy to' + ); + + // Start up the bound worker without the expected entrypoint + await dev({ + "wrangler.toml": dedent` + name = "bound" + main = "index.ts" + `, + "index.ts": dedent` + import { WorkerEntrypoint } from "cloudflare:workers"; + export class BadEntrypoint extends WorkerEntrypoint { + fetch(request) { + return new Response("bad"); + } + }; + export default {}; // Required to treat as modules format worker + `, + }); + + // Wait for error to be thrown + await waitFor(() => { + const output = session.getOutput(); + expect(output).toMatch( + 'The `wrangler dev` session for service "bound" does not export an entrypoint named "ThingEntrypoint"' + ); + }); +}); + +test("should support binding to wrangler session listening on HTTPS", async ({ + dev, +}) => { + // Start entry worker first, so the server starts with a stubbed service not + // found binding + const { url, session } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [[services]] + binding = "SERVICE" + service = "bound" + `, + "index.ts": dedent` + export default { + async fetch(request, env, ctx) { + return env.SERVICE.fetch("http://placeholder/"); + } + } + `, + }); + let response = await fetch(url); + expect(await response.text()).toBe( + '[wrangler] Couldn\'t find `wrangler dev` session for service "bound" to proxy to' + ); + + // Start up the bound worker using HTTPS + const files: Record = { + "wrangler.toml": dedent` + name = "bound" + main = "index.ts" + `, + "index.ts": dedent` + export default { + fetch() { + return new Response("secure"); + } + }; + `, + }; + await dev(files, ["--local-protocol=https"]); + + await waitFor(async () => { + const response = await fetch(url); + const text = await response.text(); + expect(text).toBe("secure"); + }); +}); + +test("should throw if binding to version of wrangler without entrypoints support over HTTPS", async ({ + dev, + isolatedDevRegistryPort, +}) => { + // Start entry worker first, so the server starts with a stubbed service not + // found binding + const { url, session } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [[services]] + binding = "SERVICE" + service = "bound" + `, + "index.ts": dedent` + export default { + async fetch(request, env, ctx) { + return env.SERVICE.fetch("http://placeholder/"); + } + } + `, + }); + let response = await fetch(url); + expect(await response.text()).toBe( + '[wrangler] Couldn\'t find `wrangler dev` session for service "bound" to proxy to' + ); + + // Simulate starting up the bound worker using HTTPS with an old version of Wrangler + response = await fetch( + `http://127.0.0.1:${isolatedDevRegistryPort}/workers/bound`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + protocol: "https", + mode: "local", + port: 0, + host: "localhost", + durableObjects: [], + durableObjectsHost: "localhost", + durableObjectsPort: 0, + // Intentionally omitting `entrypointAddresses` + }), + } + ); + expect(response.status).toBe(200); + expect(await response.text()).toBe("null"); + + // Wait for error to be thrown + await waitFor(() => { + const output = session.getOutput(); + expect(output).toMatch( + 'Cannot proxy to `wrangler dev` session for service "bound" because it uses HTTPS. Please upgrade "bound"\'s `wrangler` version, or remove the `--local-protocol`/`dev.local_protocol` option.' + ); + }); +}); + +test("should throw if performing RPC with session that hasn't started", async ({ + dev, +}) => { + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + compatibility_flags = ["rpc"] + + [[services]] + binding = "SERVICE" + service = "bound" + entrypoint = "ThingEntrypoint" + `, + "index.ts": dedent` + export default { + async fetch(request, env, ctx) { + const errors = []; + try { await env.SERVICE.property; } catch (e) { errors.push(e); } + try { await env.SERVICE.method(); } catch (e) { errors.push(e); } + return Response.json(errors.map(String)); + } + } + `, + }); + + const response = await fetch(url); + const errors = await response.json(); + expect(errors).toMatchInlineSnapshot(` + [ + "Error: Cannot access \`property\` as we couldn't find a \`wrangler dev\` session for service "bound" to proxy to.", + "Error: Cannot access \`method\` as we couldn't find a \`wrangler dev\` session for service "bound" to proxy to.", + ] + `); +}); diff --git a/fixtures/entrypoints-rpc-tests/tsconfig.json b/fixtures/entrypoints-rpc-tests/tsconfig.json new file mode 100644 index 000000000000..2fd7b413db4a --- /dev/null +++ b/fixtures/entrypoints-rpc-tests/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "include": ["**/*.ts"] +} diff --git a/fixtures/entrypoints-rpc-tests/vitest.config.ts b/fixtures/entrypoints-rpc-tests/vitest.config.ts new file mode 100644 index 000000000000..846cddc41995 --- /dev/null +++ b/fixtures/entrypoints-rpc-tests/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/entrypoints-rpc-tests/wrangler.toml b/fixtures/entrypoints-rpc-tests/wrangler.toml new file mode 100644 index 000000000000..c3395c08fcdb --- /dev/null +++ b/fixtures/entrypoints-rpc-tests/wrangler.toml @@ -0,0 +1,2 @@ +main = "src/index.mjs" +compatibility_date = "2023-12-01" diff --git a/fixtures/external-durable-objects-app/package.json b/fixtures/external-durable-objects-app/package.json index 4de2de2c96a8..a99195929a6d 100644 --- a/fixtures/external-durable-objects-app/package.json +++ b/fixtures/external-durable-objects-app/package.json @@ -12,7 +12,7 @@ "devDependencies": { "undici": "^5.28.3", "@cloudflare/workers-tsconfig": "workspace:*", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "wrangler": "workspace:*" } } diff --git a/fixtures/external-service-bindings-app/package.json b/fixtures/external-service-bindings-app/package.json index 6fa2a3906cca..f532eab73abd 100644 --- a/fixtures/external-service-bindings-app/package.json +++ b/fixtures/external-service-bindings-app/package.json @@ -18,7 +18,7 @@ "undici": "^5.28.3", "concurrently": "^8.2.1", "@cloudflare/workers-tsconfig": "workspace:*", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "wrangler": "workspace:*" } } diff --git a/fixtures/get-bindings-proxy/package.json b/fixtures/get-bindings-proxy/package.json index df1bc0e06639..3833d1abf32e 100644 --- a/fixtures/get-bindings-proxy/package.json +++ b/fixtures/get-bindings-proxy/package.json @@ -9,7 +9,7 @@ }, "devDependencies": { "@cloudflare/workers-tsconfig": "workspace:*", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "wrangler": "workspace:*", "undici": "^5.28.3" } diff --git a/fixtures/get-platform-proxy/package.json b/fixtures/get-platform-proxy/package.json index 10a7835a193a..76fb4ceaba34 100644 --- a/fixtures/get-platform-proxy/package.json +++ b/fixtures/get-platform-proxy/package.json @@ -9,7 +9,7 @@ }, "devDependencies": { "@cloudflare/workers-tsconfig": "workspace:*", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "wrangler": "workspace:*", "undici": "^5.28.3" } diff --git a/fixtures/local-mode-tests/package.json b/fixtures/local-mode-tests/package.json index f96c3e965515..a8bea2252891 100644 --- a/fixtures/local-mode-tests/package.json +++ b/fixtures/local-mode-tests/package.json @@ -14,7 +14,7 @@ }, "devDependencies": { "@cloudflare/workers-tsconfig": "workspace:*", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "@types/node": "^17.0.33", "buffer": "^6.0.3", "wrangler": "workspace:*" diff --git a/fixtures/node-app-pages/package.json b/fixtures/node-app-pages/package.json index 7a53def9fc76..04c4afcff4c8 100644 --- a/fixtures/node-app-pages/package.json +++ b/fixtures/node-app-pages/package.json @@ -15,7 +15,7 @@ }, "devDependencies": { "@cloudflare/workers-tsconfig": "workspace:*", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "undici": "^5.28.3", "wrangler": "workspace:*" }, diff --git a/fixtures/pages-dev-proxy-with-script/package.json b/fixtures/pages-dev-proxy-with-script/package.json index 48b5d52e2905..4d4a7f389821 100644 --- a/fixtures/pages-dev-proxy-with-script/package.json +++ b/fixtures/pages-dev-proxy-with-script/package.json @@ -10,7 +10,7 @@ }, "devDependencies": { "@cloudflare/workers-tsconfig": "workspace:*", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "undici": "^5.28.3", "wrangler": "workspace:*" }, diff --git a/fixtures/pages-functions-app/package.json b/fixtures/pages-functions-app/package.json index a4dd21a28c70..cd8893501fcf 100644 --- a/fixtures/pages-functions-app/package.json +++ b/fixtures/pages-functions-app/package.json @@ -12,7 +12,7 @@ }, "devDependencies": { "@cloudflare/workers-tsconfig": "workspace:*", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "pages-plugin-example": "workspace:*", "undici": "^5.28.3", "wrangler": "workspace:*" diff --git a/fixtures/pages-functions-with-routes-app/package.json b/fixtures/pages-functions-with-routes-app/package.json index bbf9fa5e8b1a..d2eb2d7e4b07 100644 --- a/fixtures/pages-functions-with-routes-app/package.json +++ b/fixtures/pages-functions-with-routes-app/package.json @@ -11,7 +11,7 @@ }, "devDependencies": { "@cloudflare/workers-tsconfig": "workspace:*", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "undici": "^5.28.3", "wrangler": "workspace:*" }, diff --git a/fixtures/pages-plugin-mounted-on-root-app/package.json b/fixtures/pages-plugin-mounted-on-root-app/package.json index bd00e31ac542..21a64bb7293b 100644 --- a/fixtures/pages-plugin-mounted-on-root-app/package.json +++ b/fixtures/pages-plugin-mounted-on-root-app/package.json @@ -12,7 +12,7 @@ }, "devDependencies": { "@cloudflare/workers-tsconfig": "workspace:*", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "pages-plugin-example": "workspace:*", "undici": "^5.28.3", "wrangler": "workspace:*" diff --git a/fixtures/pages-simple-assets/package.json b/fixtures/pages-simple-assets/package.json index dc214e538a46..ad16b84dd226 100644 --- a/fixtures/pages-simple-assets/package.json +++ b/fixtures/pages-simple-assets/package.json @@ -12,7 +12,7 @@ }, "devDependencies": { "@cloudflare/workers-tsconfig": "workspace:*", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "undici": "^5.28.3", "wrangler": "workspace:*" }, diff --git a/fixtures/shared/src/run-wrangler-long-lived.ts b/fixtures/shared/src/run-wrangler-long-lived.ts index 9d63214a8e65..9063387ba063 100644 --- a/fixtures/shared/src/run-wrangler-long-lived.ts +++ b/fixtures/shared/src/run-wrangler-long-lived.ts @@ -1,4 +1,5 @@ import { fork } from "node:child_process"; +import events from "node:events"; import path from "node:path"; export const wranglerEntryPath = path.resolve( @@ -17,12 +18,17 @@ export const wranglerEntryPath = path.resolve( export async function runWranglerPagesDev( cwd: string, publicPath: string | undefined, - options: string[] + options: string[], + env?: NodeJS.ProcessEnv ) { if (publicPath) { - return runLongLivedWrangler(["pages", "dev", publicPath, ...options], cwd); + return runLongLivedWrangler( + ["pages", "dev", publicPath, ...options], + cwd, + env + ); } else { - return runLongLivedWrangler(["pages", "dev", ...options], cwd); + return runLongLivedWrangler(["pages", "dev", ...options], cwd, env); } } @@ -34,11 +40,19 @@ export async function runWranglerPagesDev( * - `ip` and `port` of the http-server hosting the pages project * - `stop()` function that will close down the server. */ -export async function runWranglerDev(cwd: string, options: string[]) { - return runLongLivedWrangler(["dev", ...options], cwd); +export async function runWranglerDev( + cwd: string, + options: string[], + env?: NodeJS.ProcessEnv +) { + return runLongLivedWrangler(["dev", ...options], cwd, env); } -async function runLongLivedWrangler(command: string[], cwd: string) { +async function runLongLivedWrangler( + command: string[], + cwd: string, + env?: NodeJS.ProcessEnv +) { let settledReadyPromise = false; let resolveReadyPromise: (value: { ip: string; port: number }) => void; let rejectReadyPromise: (reason: unknown) => void; @@ -51,6 +65,7 @@ async function runLongLivedWrangler(command: string[], cwd: string) { const wranglerProcess = fork(wranglerEntryPath, command, { stdio: [/*stdin*/ "ignore", /*stdout*/ "pipe", /*stderr*/ "pipe", "ipc"], cwd, + env: { ...process.env, ...env, PWD: cwd }, }).on("message", (message) => { if (settledReadyPromise) return; settledReadyPromise = true; @@ -82,16 +97,10 @@ async function runLongLivedWrangler(command: string[], cwd: string) { }, 20_000); async function stop() { - return new Promise((resolve, reject) => { - wranglerProcess.once("exit", (code) => { - if (!code) { - resolve(code); - } else { - reject(code); - } - }); - wranglerProcess.kill("SIGTERM"); - }); + const closePromise = events.once(wranglerProcess, "close"); + wranglerProcess.kill("SIGTERM"); + const [code] = await closePromise; + if (code) throw new Error(`Exited with code ${code}`); } const { ip, port } = await ready; diff --git a/fixtures/worker-ts/package.json b/fixtures/worker-ts/package.json index 0dbf40ec9bda..a1444ab6db09 100644 --- a/fixtures/worker-ts/package.json +++ b/fixtures/worker-ts/package.json @@ -6,7 +6,7 @@ "start": "wrangler dev" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "wrangler": "workspace:*" } } diff --git a/package.json b/package.json index 37830b6abd48..955d514bf734 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@actions/artifact": "^2.0.1", "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.27.1", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "@turbo/gen": "^1.10.13", "@vue/compiler-sfc": "^3.3.4", "dotenv-cli": "^7.3.0", diff --git a/packages/create-cloudflare/package.json b/packages/create-cloudflare/package.json index 4b7302a1106e..25d9206cbb13 100644 --- a/packages/create-cloudflare/package.json +++ b/packages/create-cloudflare/package.json @@ -49,7 +49,7 @@ "@cloudflare/cli": "workspace:*", "@cloudflare/eslint-config-worker": "*", "@cloudflare/workers-tsconfig": "workspace:*", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "@iarna/toml": "^3.0.0", "@types/command-exists": "^1.2.0", "@types/cross-spawn": "^6.0.2", diff --git a/packages/edge-preview-authenticated-proxy/package.json b/packages/edge-preview-authenticated-proxy/package.json index 876dc4395df8..ba82cf709bdb 100644 --- a/packages/edge-preview-authenticated-proxy/package.json +++ b/packages/edge-preview-authenticated-proxy/package.json @@ -12,7 +12,7 @@ }, "devDependencies": { "@cloudflare/eslint-config-worker": "*", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "@types/cookie": "^0.6.0", "cookie": "^0.6.0", "promjs": "^0.4.2", diff --git a/packages/format-errors/package.json b/packages/format-errors/package.json index b89f17d79e58..725b5e089ea3 100644 --- a/packages/format-errors/package.json +++ b/packages/format-errors/package.json @@ -10,7 +10,7 @@ }, "devDependencies": { "@cloudflare/eslint-config-worker": "*", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "mustache": "^4.2.0", "promjs": "^0.4.2", "toucan-js": "^3.2.3", diff --git a/packages/kv-asset-handler/package.json b/packages/kv-asset-handler/package.json index 87b0967d9008..f395529f279b 100644 --- a/packages/kv-asset-handler/package.json +++ b/packages/kv-asset-handler/package.json @@ -40,7 +40,7 @@ }, "devDependencies": { "@ava/typescript": "^4.1.0", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "@types/mime": "^3.0.4", "@types/node": "^18.11.12", "ava": "^6.0.1", diff --git a/packages/miniflare/.prettierignore b/packages/miniflare/.prettierignore new file mode 100644 index 000000000000..c82652b93034 --- /dev/null +++ b/packages/miniflare/.prettierignore @@ -0,0 +1,3 @@ +dist +dist-types +src/runtime/config/workerd.capnp.* diff --git a/packages/miniflare/.prettierrc b/packages/miniflare/.prettierrc new file mode 100644 index 000000000000..b3b1329fc177 --- /dev/null +++ b/packages/miniflare/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 80, + "useTabs": true, + "trailingComma": "es5", + "proseWrap": "always" +} diff --git a/packages/miniflare/README.md b/packages/miniflare/README.md index 52f31974c950..88108095ed16 100644 --- a/packages/miniflare/README.md +++ b/packages/miniflare/README.md @@ -296,7 +296,7 @@ parameter in module format Workers. Record mapping binding name to paths containing arbitrary binary data to inject as `ArrayBuffer` bindings into this Worker. -- `serviceBindings?: Record Awaitable>` +- `serviceBindings?: Record Awaitable>` Record mapping binding name to service designators to inject as `{ fetch: typeof fetch }` @@ -307,6 +307,12 @@ parameter in module format Workers. with that `name`. - If the designator is `(await import("miniflare")).kCurrentWorker`, requests will be dispatched to the Worker defining the binding. + - If the designator is an object of the form `{ name: ..., entrypoint: ... }`, + requests will be dispatched to the entrypoint named `entrypoint` in the + Worker named `name`. The `entrypoint` defaults to `default`, meaning + `{ name: "a" }` is the same as `"a"`. If `name` is + `(await import("miniflare")).kCurrentWorker`, requests will be dispatched to + the Worker defining the binding. - If the designator is an object of the form `{ network: { ... } }`, where `network` is a [`workerd` `Network` struct](https://github.com/cloudflare/workerd/blob/bdbd6075c7c53948050c52d22f2dfa37bf376253/src/workerd/server/workerd.capnp#L555-L598), @@ -799,4 +805,5 @@ defined at the top-level. - `getCf(): Promise>` - Returns the same object returned from incoming `Request`'s `cf` property. This object depends on the `cf` property from `SharedOptions`. + Returns the same object returned from incoming `Request`'s `cf` property. This + object depends on the `cf` property from `SharedOptions`. diff --git a/packages/miniflare/src/index.ts b/packages/miniflare/src/index.ts index 44e9d3223db6..75647472f6cb 100644 --- a/packages/miniflare/src/index.ts +++ b/packages/miniflare/src/index.ts @@ -45,6 +45,7 @@ import { D1_PLUGIN_NAME, DURABLE_OBJECTS_PLUGIN_NAME, DurableObjectClassNames, + HOST_CAPNP_CONNECT, KV_PLUGIN_NAME, PLUGIN_ENTRIES, PluginServicesOptions, @@ -80,6 +81,7 @@ import { import { Config, Extension, + HttpOptions_Style, Runtime, RuntimeOptions, Service, @@ -600,32 +602,30 @@ function safeReadableStreamFrom(iterable: AsyncIterable) { // rejections from aborted request body streams: // https://github.com/nodejs/undici/blob/dfaec78f7a29f07bb043f9006ed0ceb0d5220b55/lib/core/util.js#L369-L392 let iterator: AsyncIterator; - return new ReadableStream( - { - async start() { - iterator = iterable[Symbol.asyncIterator](); - }, - // @ts-expect-error `pull` may return anything - async pull(controller): Promise { - try { - const { done, value } = await iterator.next(); - if (done) { - queueMicrotask(() => controller.close()); - } else { - const buf = Buffer.isBuffer(value) ? value : Buffer.from(value); - controller.enqueue(new Uint8Array(buf)); - } - } catch { + return new ReadableStream({ + async start() { + iterator = iterable[Symbol.asyncIterator](); + }, + // @ts-expect-error `pull` may return anything + async pull(controller): Promise { + try { + const { done, value } = await iterator.next(); + if (done) { queueMicrotask(() => controller.close()); + } else { + const buf = Buffer.isBuffer(value) ? value : Buffer.from(value); + controller.enqueue(new Uint8Array(buf)); } - // @ts-expect-error `pull` may return anything - return controller.desiredSize > 0; - }, - async cancel() { - await iterator.return?.(); - }, - } - ); + } catch { + queueMicrotask(() => controller.close()); + } + // @ts-expect-error `pull` may return anything + return controller.desiredSize > 0; + }, + async cancel() { + await iterator.return?.(); + }, + }); } // Maps `Miniflare` instances to stack traces for their construction. Used to identify un-`dispose()`d instances. @@ -639,6 +639,7 @@ export function _initialiseInstanceRegistry() { export class Miniflare { #previousSharedOpts?: PluginSharedOptions; + #previousWorkerOpts?: PluginWorkerOptions[]; #sharedOpts: PluginSharedOptions; #workerOpts: PluginWorkerOptions[]; #log: Log; @@ -1002,6 +1003,7 @@ export class Miniflare { } async #assembleConfig(loopbackPort: number): Promise { + const allPreviousWorkerOpts = this.#previousWorkerOpts; const allWorkerOpts = this.#workerOpts; const sharedOpts = this.#sharedOpts; @@ -1057,6 +1059,7 @@ export class Miniflare { }[] = []; for (let i = 0; i < allWorkerOpts.length; i++) { + const previousWorkerOpts = allPreviousWorkerOpts?.[i]; const workerOpts = allWorkerOpts[i]; const workerName = workerOpts.core.name ?? ""; const isModulesWorker = Boolean(workerOpts.core.modules); @@ -1173,26 +1176,32 @@ export class Miniflare { // Allow additional sockets to be opened directly to specific workers, // bypassing Miniflare's entry worker. - const { unsafeDirectHost, unsafeDirectPort } = workerOpts.core; - if (unsafeDirectHost !== undefined || unsafeDirectPort !== undefined) { - const name = getDirectSocketName(i); + const previousDirectSockets = + previousWorkerOpts?.core.unsafeDirectSockets ?? []; + const directSockets = workerOpts.core.unsafeDirectSockets ?? []; + for (let j = 0; j < directSockets.length; j++) { + const previousDirectSocket = previousDirectSockets[j]; + const directSocket = directSockets[j]; + const entrypoint = directSocket.entrypoint ?? "default"; + const name = getDirectSocketName(i, entrypoint); const address = this.#getSocketAddress( name, - // We don't attempt to reuse allocated ports for `unsafeDirectPort: 0` - // as there's not always a clear mapping between current/previous - // worker options. We could do it by index, names, script, etc. - // This is an unsafe option primarily intended for Wrangler's - // inspector proxy, which will usually set this value to `9229`. - // We could consider changing this in the future. - /* previousRequestedPort */ undefined, - unsafeDirectHost, - unsafeDirectPort + previousDirectSocket?.port, + directSocket.host, + directSocket.port ); sockets.push({ name, address, - service: { name: getUserServiceName(workerName) }, - http: {}, + service: { + name: getUserServiceName(workerName), + entrypoint: entrypoint === "default" ? undefined : entrypoint, + }, + http: { + style: directSocket.proxy ? HttpOptions_Style.PROXY : undefined, + cfBlobHeader: CoreHeaders.CF_BLOB, + capnpConnectHost: HOST_CAPNP_CONNECT, + }, }); } } @@ -1433,7 +1442,10 @@ export class Miniflare { return new URL(`ws://127.0.0.1:${maybePort}`); } - async unsafeGetDirectURL(workerName?: string): Promise { + async unsafeGetDirectURL( + workerName?: string, + entrypoint = "default" + ): Promise { this.#checkDisposed(); await this.ready; @@ -1442,7 +1454,7 @@ export class Miniflare { const workerOpts = this.#workerOpts[workerIndex]; // Try to get direct access port for worker - const socketName = getDirectSocketName(workerIndex); + const socketName = getDirectSocketName(workerIndex, entrypoint); // `#socketPorts` is assigned in `#assembleAndUpdateConfig()`, which is // called by `#init()`, and `ready` doesn't resolve until `#init()` returns. assert(this.#socketPorts !== undefined); @@ -1450,13 +1462,20 @@ export class Miniflare { if (maybePort === undefined) { const friendlyWorkerName = workerName === undefined ? "entrypoint" : JSON.stringify(workerName); + const friendlyEntrypointName = + entrypoint === "default" ? entrypoint : JSON.stringify(entrypoint); throw new TypeError( - `Direct access disabled in ${friendlyWorkerName} worker` + `Direct access disabled in ${friendlyWorkerName} worker for ${friendlyEntrypointName} entrypoint` ); } // Construct accessible URL from configured host and port - const host = workerOpts.core.unsafeDirectHost ?? DEFAULT_HOST; + const directSocket = workerOpts.core.unsafeDirectSockets?.find( + (socket) => (socket.entrypoint ?? "default") === entrypoint + ); + // Should be able to find socket with correct entrypoint if port assigned + assert(directSocket !== undefined); + const host = directSocket.host ?? DEFAULT_HOST; const accessibleHost = maybeGetLocallyAccessibleHost(host) ?? getURLSafeHost(host); // noinspection HttpUrlsUsage @@ -1478,6 +1497,7 @@ export class Miniflare { // Split and validate options const [sharedOpts, workerOpts] = validateOptions(opts); this.#previousSharedOpts = this.#sharedOpts; + this.#previousWorkerOpts = this.#workerOpts; this.#sharedOpts = sharedOpts; this.#workerOpts = workerOpts; this.#log = this.#sharedOpts.core.log ?? this.#log; diff --git a/packages/miniflare/src/plugins/core/index.ts b/packages/miniflare/src/plugins/core/index.ts index 786db72377a1..bea399ac892a 100644 --- a/packages/miniflare/src/plugins/core/index.ts +++ b/packages/miniflare/src/plugins/core/index.ts @@ -106,6 +106,13 @@ const WrappedBindingSchema = z.object({ // Validate as string, but don't include in parsed output const UnusableStringSchema = z.string().transform(() => undefined); +export const UnsafeDirectSocketSchema = z.object({ + host: z.ostring(), + port: z.onumber(), + entrypoint: z.ostring(), + proxy: z.oboolean(), +}); + const CoreOptionsSchemaInput = z.intersection( SourceOptionsSchema, z.object({ @@ -131,8 +138,7 @@ const CoreOptionsSchemaInput = z.intersection( // TODO(soon): remove this in favour of per-object `unsafeUniqueKey: kEphemeralUniqueKey` unsafeEphemeralDurableObjects: z.boolean().optional(), - unsafeDirectHost: z.string().optional(), - unsafeDirectPort: z.number().optional(), + unsafeDirectSockets: UnsafeDirectSocketSchema.array().optional(), unsafeEvalBinding: z.string().optional(), unsafeUseModuleFallbackService: z.boolean().optional(), @@ -224,19 +230,29 @@ function getCustomServiceDesignator( service: z.infer ): ServiceDesignator { let serviceName: string; + let entrypoint: string | undefined; if (typeof service === "function") { // Custom `fetch` function serviceName = getCustomServiceName(workerIndex, kind, name); } else if (typeof service === "object") { - // Builtin workerd service: network, external, disk - serviceName = getBuiltinServiceName(workerIndex, kind, name); + if ("name" in service) { + if (service.name === kCurrentWorker) { + serviceName = getUserServiceName(refererName); + } else { + serviceName = getUserServiceName(service.name); + } + entrypoint = service.entrypoint; + } else { + // Builtin workerd service: network, external, disk + serviceName = getBuiltinServiceName(workerIndex, kind, name); + } } else if (service === kCurrentWorker) { serviceName = getUserServiceName(refererName); } else { // Regular user worker serviceName = getUserServiceName(service); } - return { name: serviceName }; + return { name: serviceName, entrypoint }; } function maybeGetCustomServiceService( @@ -261,7 +277,7 @@ function maybeGetCustomServiceService( ], }, }; - } else if (typeof service === "object") { + } else if (typeof service === "object" && !("name" in service)) { // Builtin workerd service: network, external, disk return { name: getBuiltinServiceName(workerIndex, kind, name), diff --git a/packages/miniflare/src/plugins/core/modules.ts b/packages/miniflare/src/plugins/core/modules.ts index 0de967225800..760dc1905be4 100644 --- a/packages/miniflare/src/plugins/core/modules.ts +++ b/packages/miniflare/src/plugins/core/modules.ts @@ -257,14 +257,6 @@ export class ModuleLocator { referencingType: JavaScriptModuleRuleType, specExpression: estree.Expression | estree.SpreadElement ) { - if (maybeGetStringScriptPathIndex(referencingName) !== undefined) { - const prefix = getResolveErrorPrefix(referencingPath); - throw new MiniflareCoreError( - "ERR_MODULE_STRING_SCRIPT", - `${prefix}: imports are unsupported in string \`script\` without defined \`scriptPath\`` - ); - } - // Ensure spec is a static string literal, and resolve full module identifier if ( specExpression.type !== "Literal" || @@ -310,6 +302,16 @@ ${dim(modulesConfig)}`; return; } + // If this isn't a built-in module, and this is a string script without + // a path, we won't be able to resolve it + if (maybeGetStringScriptPathIndex(referencingName) !== undefined) { + const prefix = getResolveErrorPrefix(referencingPath); + throw new MiniflareCoreError( + "ERR_MODULE_STRING_SCRIPT", + `${prefix}: imports are unsupported in string \`script\` without defined \`scriptPath\`` + ); + } + const identifier = path.resolve(path.dirname(referencingPath), spec); const name = moduleName(this.modulesRoot, identifier); diff --git a/packages/miniflare/src/plugins/core/services.ts b/packages/miniflare/src/plugins/core/services.ts index 86d1819aac4b..06c32a924e85 100644 --- a/packages/miniflare/src/plugins/core/services.ts +++ b/packages/miniflare/src/plugins/core/services.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { Request, Response } from "../../http"; -import type { Miniflare } from "../../index"; +import { HOST_CAPNP_CONNECT, Miniflare } from "../../index"; import { ExternalServer, HttpOptions_Style, @@ -21,13 +21,18 @@ export const HttpOptionsHeaderSchema = z.object({ name: z.string(), // name should be required value: z.ostring(), // If omitted, the header will be removed }); -const HttpOptionsSchema = z.object({ - style: z.nativeEnum(HttpOptions_Style).optional(), - forwardedProtoHeader: z.ostring(), - cfBlobHeader: z.ostring(), - injectRequestHeaders: HttpOptionsHeaderSchema.array().optional(), - injectResponseHeaders: HttpOptionsHeaderSchema.array().optional(), -}); +const HttpOptionsSchema = z + .object({ + style: z.nativeEnum(HttpOptions_Style).optional(), + forwardedProtoHeader: z.ostring(), + cfBlobHeader: z.ostring(), + injectRequestHeaders: HttpOptionsHeaderSchema.array().optional(), + injectResponseHeaders: HttpOptionsHeaderSchema.array().optional(), + }) + .transform((options) => ({ + ...options, + capnpConnectHost: HOST_CAPNP_CONNECT, + })); const TlsOptionsKeypairSchema = z.object({ privateKey: z.ostring(), @@ -82,6 +87,10 @@ export const ServiceFetchSchema = z.custom< export const ServiceDesignatorSchema = z.union([ z.string(), z.literal(kCurrentWorker), + z.object({ + name: z.union([z.string(), z.literal(kCurrentWorker)]), + entrypoint: z.ostring(), + }), z.object({ network: NetworkSchema }), z.object({ external: ExternalServerSchema }), z.object({ disk: DiskDirectorySchema }), diff --git a/packages/miniflare/src/plugins/shared/constants.ts b/packages/miniflare/src/plugins/shared/constants.ts index 69768bef2219..9fe78c343b8d 100644 --- a/packages/miniflare/src/plugins/shared/constants.ts +++ b/packages/miniflare/src/plugins/shared/constants.ts @@ -10,13 +10,17 @@ export const SOCKET_ENTRY = "entry"; export const SOCKET_ENTRY_LOCAL = "entry:local"; const SOCKET_DIRECT_PREFIX = "direct"; -export function getDirectSocketName(workerIndex: number) { - return `${SOCKET_DIRECT_PREFIX}:${workerIndex}`; +export function getDirectSocketName(workerIndex: number, entrypoint: string) { + return `${SOCKET_DIRECT_PREFIX}:${workerIndex}:${entrypoint}`; } // Service looping back to Miniflare's Node.js process (for storage, etc) export const SERVICE_LOOPBACK = "loopback"; +// Special host to use for Cap'n Proto connections. This is required to use +// JS RPC over `external` services in Wrangler's service registry. +export const HOST_CAPNP_CONNECT = "miniflare-unsafe-internal-capnp-connect"; + export const WORKER_BINDING_SERVICE_LOOPBACK: Worker_Binding = { name: CoreBindings.SERVICE_LOOPBACK, service: { name: SERVICE_LOOPBACK }, diff --git a/packages/miniflare/src/runtime/config/workerd.capnp b/packages/miniflare/src/runtime/config/workerd.capnp index 863de6667b62..295a91de7efe 100644 --- a/packages/miniflare/src/runtime/config/workerd.capnp +++ b/packages/miniflare/src/runtime/config/workerd.capnp @@ -39,7 +39,6 @@ # 2. added to `tryImportBulitin` in workerd.c++ (grep for '"/workerd/workerd.capnp"'). using Cxx = import "/capnp/c++.capnp"; $Cxx.namespace("workerd::server::config"); -$Cxx.allowCancellation; struct Config { # Top-level configuration for a workerd instance. @@ -386,6 +385,17 @@ struct Worker { unsafeEval @23 :Void; # A simple binding that enables access to the UnsafeEval API. + memoryCache :group { + # A binding representing access to an in-memory cache. + + id @24 :Text; + # The identifier associated with this cache. Any number of isolates + # can access the same in-memory cache (within the same process), and + # each worker may use any number of in-memory caches. + + limits @25 :MemoryCacheLimits; + } + # TODO(someday): dispatch, other new features } @@ -483,6 +493,12 @@ struct Worker { } } + struct MemoryCacheLimits { + maxKeys @0 :UInt32; + maxValueSize @1 :UInt32; + maxTotalValueSize @2 :UInt64; + } + struct WrappedBinding { # A binding that wraps a group of (lower-level) bindings in a common API. @@ -504,7 +520,7 @@ struct Worker { } } - globalOutbound @6 :ServiceDesignator = "internet"; + globalOutbound @6 :ServiceDesignator = ( name = "internet" ); # Where should the global "fetch" go to? The default is the service called "internet", which # should usually be configured to talk to the public internet. @@ -804,6 +820,12 @@ struct HttpOptions { # If null, the header will be removed. } + capnpConnectHost @5 :Text; + # A CONNECT request for this host+port will be treated as a request to form a Cap'n Proto RPC + # connection. The server will expose a WorkerdBootstrap as the bootstrap interface, allowing + # events to be delivered to the target worker via capnp. Clients will use capnp for non-HTTP + # event types (especially JSRPC). + # TODO(someday): When we support TCP, include an option to deliver CONNECT requests to the # TCP handler. } diff --git a/packages/miniflare/src/runtime/config/workerd.capnp.d.ts b/packages/miniflare/src/runtime/config/workerd.capnp.d.ts index d16661156a5c..f0eec5e4d647 100644 --- a/packages/miniflare/src/runtime/config/workerd.capnp.d.ts +++ b/packages/miniflare/src/runtime/config/workerd.capnp.d.ts @@ -404,6 +404,20 @@ export declare class Worker_Binding_CryptoKey extends __S { toString(): string; which(): Worker_Binding_CryptoKey_Which; } +export declare class Worker_Binding_MemoryCacheLimits extends __S { + static readonly _capnp: { + displayName: string; + id: string; + size: capnp.ObjectSize; + }; + getMaxKeys(): number; + setMaxKeys(value: number): void; + getMaxValueSize(): number; + setMaxValueSize(value: number): void; + getMaxTotalValueSize(): capnp.Uint64; + setMaxTotalValueSize(value: capnp.Uint64): void; + toString(): string; +} export declare class Worker_Binding_WrappedBinding extends __S { static readonly _capnp: { displayName: string; @@ -462,6 +476,22 @@ export declare class Worker_Binding_Hyperdrive extends __S { setScheme(value: string): void; toString(): string; } +export declare class Worker_Binding_MemoryCache extends __S { + static readonly _capnp: { + displayName: string; + id: string; + size: capnp.ObjectSize; + }; + getId(): string; + setId(value: string): void; + adoptLimits(value: capnp.Orphan): void; + disownLimits(): capnp.Orphan; + getLimits(): Worker_Binding_MemoryCacheLimits; + hasLimits(): boolean; + initLimits(): Worker_Binding_MemoryCacheLimits; + setLimits(value: Worker_Binding_MemoryCacheLimits): void; + toString(): string; +} export declare enum Worker_Binding_Which { UNSPECIFIED = 0, PARAMETER = 1, @@ -480,7 +510,8 @@ export declare enum Worker_Binding_Which { FROM_ENVIRONMENT = 14, ANALYTICS_ENGINE = 15, HYPERDRIVE = 16, - UNSAFE_EVAL = 17 + UNSAFE_EVAL = 17, + MEMORY_CACHE = 18 } export declare class Worker_Binding extends __S { static readonly UNSPECIFIED = Worker_Binding_Which.UNSPECIFIED; @@ -501,9 +532,11 @@ export declare class Worker_Binding extends __S { static readonly ANALYTICS_ENGINE = Worker_Binding_Which.ANALYTICS_ENGINE; static readonly HYPERDRIVE = Worker_Binding_Which.HYPERDRIVE; static readonly UNSAFE_EVAL = Worker_Binding_Which.UNSAFE_EVAL; + static readonly MEMORY_CACHE = Worker_Binding_Which.MEMORY_CACHE; static readonly Type: typeof Worker_Binding_Type; static readonly DurableObjectNamespaceDesignator: typeof Worker_Binding_DurableObjectNamespaceDesignator; static readonly CryptoKey: typeof Worker_Binding_CryptoKey; + static readonly MemoryCacheLimits: typeof Worker_Binding_MemoryCacheLimits; static readonly WrappedBinding: typeof Worker_Binding_WrappedBinding; static readonly _capnp: { displayName: string; @@ -610,6 +643,10 @@ export declare class Worker_Binding extends __S { setHyperdrive(): void; isUnsafeEval(): boolean; setUnsafeEval(): void; + getMemoryCache(): Worker_Binding_MemoryCache; + initMemoryCache(): Worker_Binding_MemoryCache; + isMemoryCache(): boolean; + setMemoryCache(): void; toString(): string; which(): Worker_Binding_Which; } @@ -896,6 +933,8 @@ export declare class HttpOptions extends __S { hasInjectResponseHeaders(): boolean; initInjectResponseHeaders(length: number): capnp.List; setInjectResponseHeaders(value: capnp.List): void; + getCapnpConnectHost(): string; + setCapnpConnectHost(value: string): void; toString(): string; } export declare class TlsOptions_Keypair extends __S { diff --git a/packages/miniflare/src/runtime/config/workerd.capnp.js b/packages/miniflare/src/runtime/config/workerd.capnp.js index c1d8fa74308e..bb8be0dc6c1d 100644 --- a/packages/miniflare/src/runtime/config/workerd.capnp.js +++ b/packages/miniflare/src/runtime/config/workerd.capnp.js @@ -1,7 +1,7 @@ "use strict"; /* tslint:disable */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.Extension = exports.Extension_Module = exports.TlsOptions = exports.TlsOptions_Version = exports.TlsOptions_Keypair = exports.HttpOptions = exports.HttpOptions_Header = exports.HttpOptions_Style = exports.DiskDirectory = exports.Network = exports.ExternalServer = exports.ExternalServer_Which = exports.ExternalServer_Tcp = exports.ExternalServer_Https = exports.Worker = exports.Worker_Which = exports.Worker_DurableObjectStorage = exports.Worker_DurableObjectStorage_Which = exports.Worker_DurableObjectNamespace = exports.Worker_DurableObjectNamespace_Which = exports.Worker_Binding = exports.Worker_Binding_Which = exports.Worker_Binding_Hyperdrive = exports.Worker_Binding_Parameter = exports.Worker_Binding_WrappedBinding = exports.Worker_Binding_CryptoKey = exports.Worker_Binding_CryptoKey_Which = exports.Worker_Binding_CryptoKey_Algorithm = exports.Worker_Binding_CryptoKey_Algorithm_Which = exports.Worker_Binding_CryptoKey_Usage = exports.Worker_Binding_DurableObjectNamespaceDesignator = exports.Worker_Binding_Type = exports.Worker_Binding_Type_Which = exports.Worker_Module = exports.Worker_Module_Which = exports.ServiceDesignator = exports.Service = exports.Service_Which = exports.Socket = exports.Socket_Which = exports.Socket_Https = exports.Config = exports._capnpFileId = void 0; +exports.Extension = exports.Extension_Module = exports.TlsOptions = exports.TlsOptions_Version = exports.TlsOptions_Keypair = exports.HttpOptions = exports.HttpOptions_Header = exports.HttpOptions_Style = exports.DiskDirectory = exports.Network = exports.ExternalServer = exports.ExternalServer_Which = exports.ExternalServer_Tcp = exports.ExternalServer_Https = exports.Worker = exports.Worker_Which = exports.Worker_DurableObjectStorage = exports.Worker_DurableObjectStorage_Which = exports.Worker_DurableObjectNamespace = exports.Worker_DurableObjectNamespace_Which = exports.Worker_Binding = exports.Worker_Binding_Which = exports.Worker_Binding_MemoryCache = exports.Worker_Binding_Hyperdrive = exports.Worker_Binding_Parameter = exports.Worker_Binding_WrappedBinding = exports.Worker_Binding_MemoryCacheLimits = exports.Worker_Binding_CryptoKey = exports.Worker_Binding_CryptoKey_Which = exports.Worker_Binding_CryptoKey_Algorithm = exports.Worker_Binding_CryptoKey_Algorithm_Which = exports.Worker_Binding_CryptoKey_Usage = exports.Worker_Binding_DurableObjectNamespaceDesignator = exports.Worker_Binding_Type = exports.Worker_Binding_Type_Which = exports.Worker_Module = exports.Worker_Module_Which = exports.ServiceDesignator = exports.Service = exports.Service_Which = exports.Socket = exports.Socket_Which = exports.Socket_Https = exports.Config = exports._capnpFileId = void 0; /** * This file has been automatically generated by the [capnpc-ts utility](https://github.com/jdiaz5513/capnp-ts). */ @@ -579,6 +579,17 @@ Worker_Binding_CryptoKey.SPKI = Worker_Binding_CryptoKey_Which.SPKI; Worker_Binding_CryptoKey.JWK = Worker_Binding_CryptoKey_Which.JWK; Worker_Binding_CryptoKey.Usage = Worker_Binding_CryptoKey_Usage; Worker_Binding_CryptoKey._capnp = { displayName: "CryptoKey", id: "b5e1bff0e57d6eb0", size: new capnp_ts_1.ObjectSize(8, 3), defaultExtractable: capnp.getBitMask(false, 0) }; +class Worker_Binding_MemoryCacheLimits extends capnp_ts_1.Struct { + getMaxKeys() { return capnp_ts_1.Struct.getUint32(0, this); } + setMaxKeys(value) { capnp_ts_1.Struct.setUint32(0, value, this); } + getMaxValueSize() { return capnp_ts_1.Struct.getUint32(4, this); } + setMaxValueSize(value) { capnp_ts_1.Struct.setUint32(4, value, this); } + getMaxTotalValueSize() { return capnp_ts_1.Struct.getUint64(8, this); } + setMaxTotalValueSize(value) { capnp_ts_1.Struct.setUint64(8, value, this); } + toString() { return "Worker_Binding_MemoryCacheLimits_" + super.toString(); } +} +exports.Worker_Binding_MemoryCacheLimits = Worker_Binding_MemoryCacheLimits; +Worker_Binding_MemoryCacheLimits._capnp = { displayName: "MemoryCacheLimits", id: "8d66725b0867e634", size: new capnp_ts_1.ObjectSize(16, 0) }; class Worker_Binding_WrappedBinding extends capnp_ts_1.Struct { getModuleName() { return capnp_ts_1.Struct.getText(0, this); } setModuleName(value) { capnp_ts_1.Struct.setText(0, value, this); } @@ -626,6 +637,19 @@ class Worker_Binding_Hyperdrive extends capnp_ts_1.Struct { } exports.Worker_Binding_Hyperdrive = Worker_Binding_Hyperdrive; Worker_Binding_Hyperdrive._capnp = { displayName: "hyperdrive", id: "ad6c391cd55f3134", size: new capnp_ts_1.ObjectSize(8, 6) }; +class Worker_Binding_MemoryCache extends capnp_ts_1.Struct { + getId() { return capnp_ts_1.Struct.getText(1, this); } + setId(value) { capnp_ts_1.Struct.setText(1, value, this); } + adoptLimits(value) { capnp_ts_1.Struct.adopt(value, capnp_ts_1.Struct.getPointer(2, this)); } + disownLimits() { return capnp_ts_1.Struct.disown(this.getLimits()); } + getLimits() { return capnp_ts_1.Struct.getStruct(2, Worker_Binding_MemoryCacheLimits, this); } + hasLimits() { return !capnp_ts_1.Struct.isNull(capnp_ts_1.Struct.getPointer(2, this)); } + initLimits() { return capnp_ts_1.Struct.initStructAt(2, Worker_Binding_MemoryCacheLimits, this); } + setLimits(value) { capnp_ts_1.Struct.copyFrom(value, capnp_ts_1.Struct.getPointer(2, this)); } + toString() { return "Worker_Binding_MemoryCache_" + super.toString(); } +} +exports.Worker_Binding_MemoryCache = Worker_Binding_MemoryCache; +Worker_Binding_MemoryCache._capnp = { displayName: "memoryCache", id: "aed5760c349869da", size: new capnp_ts_1.ObjectSize(8, 6) }; var Worker_Binding_Which; (function (Worker_Binding_Which) { Worker_Binding_Which[Worker_Binding_Which["UNSPECIFIED"] = 0] = "UNSPECIFIED"; @@ -646,6 +670,7 @@ var Worker_Binding_Which; Worker_Binding_Which[Worker_Binding_Which["ANALYTICS_ENGINE"] = 15] = "ANALYTICS_ENGINE"; Worker_Binding_Which[Worker_Binding_Which["HYPERDRIVE"] = 16] = "HYPERDRIVE"; Worker_Binding_Which[Worker_Binding_Which["UNSAFE_EVAL"] = 17] = "UNSAFE_EVAL"; + Worker_Binding_Which[Worker_Binding_Which["MEMORY_CACHE"] = 18] = "MEMORY_CACHE"; })(Worker_Binding_Which = exports.Worker_Binding_Which || (exports.Worker_Binding_Which = {})); class Worker_Binding extends capnp_ts_1.Struct { getName() { return capnp_ts_1.Struct.getText(0, this); } @@ -910,6 +935,16 @@ class Worker_Binding extends capnp_ts_1.Struct { setHyperdrive() { capnp_ts_1.Struct.setUint16(0, 16, this); } isUnsafeEval() { return capnp_ts_1.Struct.getUint16(0, this) === 17; } setUnsafeEval() { capnp_ts_1.Struct.setUint16(0, 17, this); } + getMemoryCache() { + capnp_ts_1.Struct.testWhich("memoryCache", capnp_ts_1.Struct.getUint16(0, this), 18, this); + return capnp_ts_1.Struct.getAs(Worker_Binding_MemoryCache, this); + } + initMemoryCache() { + capnp_ts_1.Struct.setUint16(0, 18, this); + return capnp_ts_1.Struct.getAs(Worker_Binding_MemoryCache, this); + } + isMemoryCache() { return capnp_ts_1.Struct.getUint16(0, this) === 18; } + setMemoryCache() { capnp_ts_1.Struct.setUint16(0, 18, this); } toString() { return "Worker_Binding_" + super.toString(); } which() { return capnp_ts_1.Struct.getUint16(0, this); } } @@ -932,9 +967,11 @@ Worker_Binding.FROM_ENVIRONMENT = Worker_Binding_Which.FROM_ENVIRONMENT; Worker_Binding.ANALYTICS_ENGINE = Worker_Binding_Which.ANALYTICS_ENGINE; Worker_Binding.HYPERDRIVE = Worker_Binding_Which.HYPERDRIVE; Worker_Binding.UNSAFE_EVAL = Worker_Binding_Which.UNSAFE_EVAL; +Worker_Binding.MEMORY_CACHE = Worker_Binding_Which.MEMORY_CACHE; Worker_Binding.Type = Worker_Binding_Type; Worker_Binding.DurableObjectNamespaceDesignator = Worker_Binding_DurableObjectNamespaceDesignator; Worker_Binding.CryptoKey = Worker_Binding_CryptoKey; +Worker_Binding.MemoryCacheLimits = Worker_Binding_MemoryCacheLimits; Worker_Binding.WrappedBinding = Worker_Binding_WrappedBinding; Worker_Binding._capnp = { displayName: "Binding", id: "8e7e492fd7e35f3e", size: new capnp_ts_1.ObjectSize(8, 6) }; var Worker_DurableObjectNamespace_Which; @@ -1241,12 +1278,14 @@ class HttpOptions extends capnp_ts_1.Struct { hasInjectResponseHeaders() { return !capnp_ts_1.Struct.isNull(capnp_ts_1.Struct.getPointer(3, this)); } initInjectResponseHeaders(length) { return capnp_ts_1.Struct.initList(3, HttpOptions._InjectResponseHeaders, length, this); } setInjectResponseHeaders(value) { capnp_ts_1.Struct.copyFrom(value, capnp_ts_1.Struct.getPointer(3, this)); } + getCapnpConnectHost() { return capnp_ts_1.Struct.getText(4, this); } + setCapnpConnectHost(value) { capnp_ts_1.Struct.setText(4, value, this); } toString() { return "HttpOptions_" + super.toString(); } } exports.HttpOptions = HttpOptions; HttpOptions.Style = HttpOptions_Style; HttpOptions.Header = HttpOptions_Header; -HttpOptions._capnp = { displayName: "HttpOptions", id: "aa8dc6885da78f19", size: new capnp_ts_1.ObjectSize(8, 4), defaultStyle: capnp.getUint16Mask(0) }; +HttpOptions._capnp = { displayName: "HttpOptions", id: "aa8dc6885da78f19", size: new capnp_ts_1.ObjectSize(8, 5), defaultStyle: capnp.getUint16Mask(0) }; class TlsOptions_Keypair extends capnp_ts_1.Struct { getPrivateKey() { return capnp_ts_1.Struct.getText(0, this); } setPrivateKey(value) { capnp_ts_1.Struct.setText(0, value, this); } diff --git a/packages/miniflare/src/runtime/config/workerd.ts b/packages/miniflare/src/runtime/config/workerd.ts index b744778899bb..a7bcc1aa6d8c 100644 --- a/packages/miniflare/src/runtime/config/workerd.ts +++ b/packages/miniflare/src/runtime/config/workerd.ts @@ -160,6 +160,17 @@ export interface Worker_Binding_Hyperdrive { scheme?: string; } +export interface Worker_Binding_MemoryCache { + id?: string; + limits?: Worker_Binding_MemoryCacheLimits; +} + +export interface Worker_Binding_MemoryCacheLimits { + maxKeys?: number; + maxValueSize?: number; + maxTotalValueSize?: number; +} + export type Worker_DurableObjectNamespace = { className?: string; preventEviction?: boolean; @@ -199,6 +210,7 @@ export interface HttpOptions { cfBlobHeader?: string; injectRequestHeaders?: HttpOptions_Header[]; injectResponseHeaders?: HttpOptions_Header[]; + capnpConnectHost?: string; } export interface HttpOptions_Header { diff --git a/packages/miniflare/test/index.spec.ts b/packages/miniflare/test/index.spec.ts index 25854c250b00..67f466eeaca9 100644 --- a/packages/miniflare/test/index.spec.ts +++ b/packages/miniflare/test/index.spec.ts @@ -512,6 +512,113 @@ test("Miniflare: service binding to current worker", async (t) => { const res = await mf.dispatchFetch("http://localhost"); t.is(await res.text(), "body:callback"); }); +test("Miniflare: service binding to network", async (t) => { + const { http } = await useServer(t, (req, res) => res.end("network")); + const mf = new Miniflare({ + serviceBindings: { NETWORK: { network: { allow: ["private"] } } }, + modules: true, + script: `export default { + fetch(request, env) { return env.NETWORK.fetch(request); } + }`, + }); + t.teardown(() => mf.dispose()); + + const res = await mf.dispatchFetch(http); + t.is(await res.text(), "network"); +}); +test("Miniflare: service binding to external server", async (t) => { + const { http } = await useServer(t, (req, res) => res.end("external")); + const mf = new Miniflare({ + serviceBindings: { + EXTERNAL: { external: { address: http.host, http: {} } }, + }, + modules: true, + script: `export default { + fetch(request, env) { return env.EXTERNAL.fetch(request); } + }`, + }); + t.teardown(() => mf.dispose()); + + const res = await mf.dispatchFetch("https://example.com"); + t.is(await res.text(), "external"); +}); +test("Miniflare: service binding to disk", async (t) => { + const tmp = await useTmp(t); + const testPath = path.join(tmp, "test.txt"); + await fs.writeFile(testPath, "👋"); + const mf = new Miniflare({ + serviceBindings: { + DISK: { disk: { path: tmp, writable: true } }, + }, + modules: true, + script: `export default { + fetch(request, env) { return env.DISK.fetch(request); } + }`, + }); + t.teardown(() => mf.dispose()); + + let res = await mf.dispatchFetch("https://example.com/test.txt"); + t.is(await res.text(), "👋"); + + res = await mf.dispatchFetch("https://example.com/test.txt", { + method: "PUT", + body: "✏️", + }); + t.is(res.status, 204); + t.is(await fs.readFile(testPath, "utf8"), "✏️"); +}); +test("Miniflare: service binding to named entrypoint", async (t) => { + const mf = new Miniflare({ + workers: [ + { + name: "a", + serviceBindings: { + A_RPC_SERVICE: { name: kCurrentWorker, entrypoint: "RpcEntrypoint" }, + A_NAMED_SERVICE: { name: "a", entrypoint: "namedEntrypoint" }, + B_NAMED_SERVICE: { name: "b", entrypoint: "anotherNamedEntrypoint" }, + }, + compatibilityFlags: ["rpc"], + modules: true, + script: ` + import { WorkerEntrypoint } from "cloudflare:workers"; + export class RpcEntrypoint extends WorkerEntrypoint { + ping() { return "a:rpc:pong"; } + } + + export const namedEntrypoint = { + fetch(request, env, ctx) { return new Response("a:named:pong"); } + }; + + export default { + async fetch(request, env) { + const aRpc = await env.A_RPC_SERVICE.ping(); + const aNamed = await (await env.A_NAMED_SERVICE.fetch("http://placeholder")).text(); + const bNamed = await (await env.B_NAMED_SERVICE.fetch("http://placeholder")).text(); + return Response.json({ aRpc, aNamed, bNamed }); + } + } + `, + }, + { + name: "b", + modules: true, + script: ` + export const anotherNamedEntrypoint = { + fetch(request, env, ctx) { return new Response("b:named:pong"); } + }; + `, + }, + ], + }); + t.teardown(() => mf.dispose()); + + const res = await mf.dispatchFetch("http://placeholder"); + t.deepEqual(await res.json(), { + aRpc: "a:rpc:pong", + aNamed: "a:named:pong", + bNamed: "b:named:pong", + }); +}); test("Miniflare: custom outbound service", async (t) => { const mf = new Miniflare({ @@ -1335,7 +1442,7 @@ test("Miniflare: allows direct access to workers", async (t) => { { name: "a", script: `addEventListener("fetch", (e) => e.respondWith(new Response("a")))`, - unsafeDirectPort: 0, + unsafeDirectSockets: [{ port: 0 }], }, { routes: ["*/*"], @@ -1344,7 +1451,25 @@ test("Miniflare: allows direct access to workers", async (t) => { { name: "c", script: `addEventListener("fetch", (e) => e.respondWith(new Response("c")))`, - unsafeDirectHost: "127.0.0.1", + unsafeDirectSockets: [{ host: "127.0.0.1" }], + }, + { + name: "d", + compatibilityFlags: ["experimental"], + modules: true, + script: ` + import { WorkerEntrypoint } from "cloudflare:workers"; + export class One extends WorkerEntrypoint { + fetch() { return new Response("d:1"); } + } + export const two = { + fetch() { return new Response("d:2"); } + }; + export const three = { + fetch() { return new Response("d:2"); } + }; + `, + unsafeDirectSockets: [{ entrypoint: "One" }, { entrypoint: "two" }], }, ], }); @@ -1363,15 +1488,63 @@ test("Miniflare: allows direct access to workers", async (t) => { res = await fetch(cURL); t.is(await res.text(), "c"); + // Check can access workers directly with different entrypoints + const d1URL = await mf.unsafeGetDirectURL("d", "One"); + const d2URL = await mf.unsafeGetDirectURL("d", "two"); + res = await fetch(d1URL); + t.is(await res.text(), "d:1"); + res = await fetch(d2URL); + t.is(await res.text(), "d:2"); + // Can can only access configured for direct access - await t.throwsAsync(mf.unsafeGetDirectURL("d"), { + await t.throwsAsync(mf.unsafeGetDirectURL("z"), { instanceOf: TypeError, - message: '"d" worker not found', + message: '"z" worker not found', }); await t.throwsAsync(mf.unsafeGetDirectURL(""), { instanceOf: TypeError, - message: 'Direct access disabled in "" worker', + message: 'Direct access disabled in "" worker for default entrypoint', + }); + await t.throwsAsync(mf.unsafeGetDirectURL("d", "three"), { + instanceOf: TypeError, + message: 'Direct access disabled in "d" worker for "three" entrypoint', + }); +}); +test("Miniflare: allows RPC between multiple instances", async (t) => { + const mf1 = new Miniflare({ + unsafeDirectSockets: [{ entrypoint: "TestEntrypoint" }], + compatibilityFlags: ["experimental"], + modules: true, + script: ` + import { WorkerEntrypoint } from "cloudflare:workers"; + export class TestEntrypoint extends WorkerEntrypoint { + ping() { return "pong"; } + } + `, + }); + t.teardown(() => mf1.dispose()); + + const testEntrypointUrl = await mf1.unsafeGetDirectURL("", "TestEntrypoint"); + + const mf2 = new Miniflare({ + serviceBindings: { + SERVICE: { external: { address: testEntrypointUrl.host, http: {} } }, + }, + compatibilityFlags: ["experimental"], + modules: true, + script: ` + export default { + async fetch(request, env, ctx) { + const result = await env.SERVICE.ping(); + return new Response(result); + } + } + `, }); + t.teardown(() => mf2.dispose()); + + const res = await mf2.dispatchFetch("http://placeholder"); + t.is(await res.text(), "pong"); }); // Only test `MINIFLARE_WORKERD_PATH` on Unix. The test uses a Node.js script diff --git a/packages/pages-shared/package.json b/packages/pages-shared/package.json index a96ff1e5d103..a2c5058591f0 100644 --- a/packages/pages-shared/package.json +++ b/packages/pages-shared/package.json @@ -22,7 +22,7 @@ }, "devDependencies": { "@cloudflare/workers-tsconfig": "workspace:*", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "@types/service-worker-mock": "^2.0.1", "concurrently": "^7.3.0", "glob": "^8.0.3", diff --git a/packages/playground-preview-worker/package.json b/packages/playground-preview-worker/package.json index 4f62e05ab0a5..e208d6045b6d 100644 --- a/packages/playground-preview-worker/package.json +++ b/packages/playground-preview-worker/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@cloudflare/eslint-config-worker": "workspace:*", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "@types/cookie": "^0.5.1", "cookie": "^0.5.0", "itty-router": "^4.0.13", diff --git a/packages/prerelease-registry/package.json b/packages/prerelease-registry/package.json index b6ca9225676c..097000e726c4 100644 --- a/packages/prerelease-registry/package.json +++ b/packages/prerelease-registry/package.json @@ -18,7 +18,7 @@ "devDependencies": { "@cloudflare/eslint-config-worker": "*", "@cloudflare/workers-tsconfig": "workspace:*", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "typescript": "^4.5.5", "wrangler": "workspace:*" } diff --git a/packages/quick-edit-extension/package.json b/packages/quick-edit-extension/package.json index bc86cef8771f..ebd3618bc313 100644 --- a/packages/quick-edit-extension/package.json +++ b/packages/quick-edit-extension/package.json @@ -41,7 +41,7 @@ ], "devDependencies": { "@cloudflare/workers-tsconfig": "workspace:^", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "esbuild": "0.16.3", "esbuild-register": "^3.4.2", "typescript": "^4.9.5" diff --git a/packages/workers.new/package.json b/packages/workers.new/package.json index c4f3d159a602..8e9b88b3ad24 100644 --- a/packages/workers.new/package.json +++ b/packages/workers.new/package.json @@ -12,7 +12,7 @@ "devDependencies": { "@cloudflare/eslint-config-worker": "*", "@cloudflare/workers-tsconfig": "workspace:*", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "wrangler": "workspace:*" } } diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index d37177237bd5..6b4c86258ac3 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -123,7 +123,7 @@ "@cloudflare/pages-shared": "workspace:^", "@cloudflare/types": "^6.18.4", "@cloudflare/workers-tsconfig": "workspace:*", - "@cloudflare/workers-types": "^4.20240320.1", + "@cloudflare/workers-types": "^4.20240402.0", "@cspotcode/source-map-support": "0.8.1", "@iarna/toml": "^3.0.0", "@microsoft/api-extractor": "^7.28.3", @@ -160,6 +160,7 @@ "concurrently": "^7.2.2", "devtools-protocol": "^0.0.955664", "dotenv": "^16.0.0", + "es-module-lexer": "^1.3.0", "esbuild-jest": "0.5.0", "execa": "^6.1.0", "express": "^4.18.1", @@ -212,7 +213,7 @@ "fsevents": "~2.3.2" }, "peerDependencies": { - "@cloudflare/workers-types": "^4.20240320.1" + "@cloudflare/workers-types": "^4.20240402.0" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { diff --git a/packages/wrangler/src/__tests__/configuration.test.ts b/packages/wrangler/src/__tests__/configuration.test.ts index eb4d319102cc..1ac442ec881d 100644 --- a/packages/wrangler/src/__tests__/configuration.test.ts +++ b/packages/wrangler/src/__tests__/configuration.test.ts @@ -938,6 +938,7 @@ describe("normalizeAndValidateConfig()", () => { binding: "SERVICE_BINDING_1", service: "SERVICE_TYPE_1", environment: "SERVICE_BINDING_ENVIRONMENT_1", + entrypoint: "SERVICE_BINDING_ENVIRONMENT_1", }, ], analytics_engine_datasets: [ @@ -2538,6 +2539,17 @@ describe("normalizeAndValidateConfig()", () => { service: 456, environment: "SERVICE_BINDING_ENVIRONMENT_1", }, + { + binding: "SERVICE_BINDING_1", + service: "SERVICE_BINDING_SERVICE_1", + environment: "SERVICE_BINDING_ENVIRONMENT_1", + entrypoint: 123, + }, + { + binding: "SERVICE_BINDING_1", + service: "SERVICE_BINDING_SERVICE_1", + entrypoint: 123, + }, ], } as unknown as RawConfig, undefined, @@ -2551,22 +2563,24 @@ describe("normalizeAndValidateConfig()", () => { " `); expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` - "Processing wrangler configuration: - - \\"services[0]\\" bindings should have a string \\"binding\\" field but got {}. - - \\"services[0]\\" bindings should have a string \\"service\\" field but got {}. - - \\"services[1]\\" bindings should have a string \\"service\\" field but got {\\"binding\\":\\"SERVICE_BINDING_1\\"}. - - \\"services[2]\\" bindings should have a string \\"binding\\" field but got {\\"binding\\":123,\\"service\\":456}. - - \\"services[2]\\" bindings should have a string \\"service\\" field but got {\\"binding\\":123,\\"service\\":456}. - - \\"services[3]\\" bindings should have a string \\"binding\\" field but got {\\"binding\\":123,\\"service\\":456,\\"environment\\":789}. - - \\"services[3]\\" bindings should have a string \\"service\\" field but got {\\"binding\\":123,\\"service\\":456,\\"environment\\":789}. - - \\"services[3]\\" bindings should have a string \\"environment\\" field but got {\\"binding\\":123,\\"service\\":456,\\"environment\\":789}. - - \\"services[4]\\" bindings should have a string \\"service\\" field but got {\\"binding\\":\\"SERVICE_BINDING_1\\",\\"service\\":456,\\"environment\\":789}. - - \\"services[4]\\" bindings should have a string \\"environment\\" field but got {\\"binding\\":\\"SERVICE_BINDING_1\\",\\"service\\":456,\\"environment\\":789}. - - \\"services[5]\\" bindings should have a string \\"binding\\" field but got {\\"binding\\":123,\\"service\\":\\"SERVICE_BINDING_SERVICE_1\\",\\"environment\\":789}. - - \\"services[5]\\" bindings should have a string \\"environment\\" field but got {\\"binding\\":123,\\"service\\":\\"SERVICE_BINDING_SERVICE_1\\",\\"environment\\":789}. - - \\"services[6]\\" bindings should have a string \\"binding\\" field but got {\\"binding\\":123,\\"service\\":456,\\"environment\\":\\"SERVICE_BINDING_ENVIRONMENT_1\\"}. - - \\"services[6]\\" bindings should have a string \\"service\\" field but got {\\"binding\\":123,\\"service\\":456,\\"environment\\":\\"SERVICE_BINDING_ENVIRONMENT_1\\"}." - `); + "Processing wrangler configuration: + - \\"services[0]\\" bindings should have a string \\"binding\\" field but got {}. + - \\"services[0]\\" bindings should have a string \\"service\\" field but got {}. + - \\"services[1]\\" bindings should have a string \\"service\\" field but got {\\"binding\\":\\"SERVICE_BINDING_1\\"}. + - \\"services[2]\\" bindings should have a string \\"binding\\" field but got {\\"binding\\":123,\\"service\\":456}. + - \\"services[2]\\" bindings should have a string \\"service\\" field but got {\\"binding\\":123,\\"service\\":456}. + - \\"services[3]\\" bindings should have a string \\"binding\\" field but got {\\"binding\\":123,\\"service\\":456,\\"environment\\":789}. + - \\"services[3]\\" bindings should have a string \\"service\\" field but got {\\"binding\\":123,\\"service\\":456,\\"environment\\":789}. + - \\"services[3]\\" bindings should have a string \\"environment\\" field but got {\\"binding\\":123,\\"service\\":456,\\"environment\\":789}. + - \\"services[4]\\" bindings should have a string \\"service\\" field but got {\\"binding\\":\\"SERVICE_BINDING_1\\",\\"service\\":456,\\"environment\\":789}. + - \\"services[4]\\" bindings should have a string \\"environment\\" field but got {\\"binding\\":\\"SERVICE_BINDING_1\\",\\"service\\":456,\\"environment\\":789}. + - \\"services[5]\\" bindings should have a string \\"binding\\" field but got {\\"binding\\":123,\\"service\\":\\"SERVICE_BINDING_SERVICE_1\\",\\"environment\\":789}. + - \\"services[5]\\" bindings should have a string \\"environment\\" field but got {\\"binding\\":123,\\"service\\":\\"SERVICE_BINDING_SERVICE_1\\",\\"environment\\":789}. + - \\"services[6]\\" bindings should have a string \\"binding\\" field but got {\\"binding\\":123,\\"service\\":456,\\"environment\\":\\"SERVICE_BINDING_ENVIRONMENT_1\\"}. + - \\"services[6]\\" bindings should have a string \\"service\\" field but got {\\"binding\\":123,\\"service\\":456,\\"environment\\":\\"SERVICE_BINDING_ENVIRONMENT_1\\"}. + - \\"services[7]\\" bindings should have a string \\"entrypoint\\" field but got {\\"binding\\":\\"SERVICE_BINDING_1\\",\\"service\\":\\"SERVICE_BINDING_SERVICE_1\\",\\"environment\\":\\"SERVICE_BINDING_ENVIRONMENT_1\\",\\"entrypoint\\":123}. + - \\"services[8]\\" bindings should have a string \\"entrypoint\\" field but got {\\"binding\\":\\"SERVICE_BINDING_1\\",\\"service\\":\\"SERVICE_BINDING_SERVICE_1\\",\\"entrypoint\\":123}." + `); }); }); diff --git a/packages/wrangler/src/__tests__/deploy.test.ts b/packages/wrangler/src/__tests__/deploy.test.ts index 0a998ad00caf..8226a5d0418b 100644 --- a/packages/wrangler/src/__tests__/deploy.test.ts +++ b/packages/wrangler/src/__tests__/deploy.test.ts @@ -7066,6 +7066,49 @@ addEventListener('fetch', event => {});` Current Deployment ID: Galaxy-Class + NOTE: \\"Deployment ID\\" in this output will be changed to \\"Version ID\\" in a future version of Wrangler. To learn more visit: https://developers.cloudflare.com/workers/configuration/versions-and-deployments" + `); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.warn).toMatchInlineSnapshot(`""`); + }); + + it("should support service bindings with entrypoints", async () => { + writeWranglerToml({ + services: [ + { + binding: "FOO", + service: "foo-service", + environment: "production", + entrypoint: "MyHandler", + }, + ], + }); + writeWorkerSource(); + mockSubDomainRequest(); + mockUploadWorkerRequest({ + expectedBindings: [ + { + type: "service", + name: "FOO", + service: "foo-service", + environment: "production", + entrypoint: "MyHandler", + }, + ], + }); + + await runWrangler("deploy index.js"); + expect(std.out).toMatchInlineSnapshot(` + "Total Upload: xx KiB / gzip: xx KiB + Your worker has access to the following bindings: + - Services: + - FOO: foo-service - production (#MyHandler) + Uploaded test-name (TIMINGS) + Published test-name (TIMINGS) + https://test-name.test-sub-domain.workers.dev + Current Deployment ID: Galaxy-Class + + NOTE: \\"Deployment ID\\" in this output will be changed to \\"Version ID\\" in a future version of Wrangler. To learn more visit: https://developers.cloudflare.com/workers/configuration/versions-and-deployments" `); expect(std.err).toMatchInlineSnapshot(`""`); diff --git a/packages/wrangler/src/__tests__/init.test.ts b/packages/wrangler/src/__tests__/init.test.ts index 22aa99fa3a01..02d9c6d91d47 100644 --- a/packages/wrangler/src/__tests__/init.test.ts +++ b/packages/wrangler/src/__tests__/init.test.ts @@ -2516,6 +2516,7 @@ describe("init", () => { name: "website", service: "website", type: "service", + entrypoint: "WWWHandler", }, { type: "dispatch_namespace", @@ -2686,6 +2687,7 @@ describe("init", () => { environment: "production", binding: "website", service: "website", + entrypoint: "WWWHandler", }, ], triggers: { @@ -3158,6 +3160,7 @@ describe("init", () => { binding = \\"website\\" service = \\"website\\" environment = \\"production\\" + entrypoint = \\"WWWHandler\\" [[dispatch_namespaces]] binding = \\"name-namespace-mock\\" diff --git a/packages/wrangler/src/__tests__/middleware.test.ts b/packages/wrangler/src/__tests__/middleware.test.ts index 0f0e9a19418d..ced288231c81 100644 --- a/packages/wrangler/src/__tests__/middleware.test.ts +++ b/packages/wrangler/src/__tests__/middleware.test.ts @@ -798,26 +798,11 @@ describe("middleware", () => { }; - var envWrappers = [].filter(Boolean); - var facade = { - ...src_default, - envWrappers, - middleware: [ - , - ...src_default.middleware ? src_default.middleware : [] - ].filter(Boolean) - }; - var maskDurableObjectDefinition = (cls) => class extends cls { - constructor(state, env) { - let wrappedEnv = env; - for (const wrapFn of envWrappers) { - wrappedEnv = wrapFn(wrappedEnv); - } - super(state, wrappedEnv); - } - }; - var DurableObjectExample2 = maskDurableObjectDefinition(DurableObjectExample); - var middleware_insertion_facade_default = facade; + src_default.middleware = [ + , + ...src_default.middleware ?? [] + ].filter(Boolean); + var middleware_insertion_facade_default = src_default; var __facade_middleware__ = []; @@ -856,78 +841,84 @@ describe("middleware", () => { this.#noRetry(); } }; - var __facade_modules_fetch__ = function(request, env, ctx) { - if (middleware_insertion_facade_default.fetch === void 0) - throw new Error(\\"Handler does not export a fetch() function.\\"); - return middleware_insertion_facade_default.fetch(request, env, ctx); - }; - function getMaskedEnv(rawEnv) { - let env = rawEnv; - if (middleware_insertion_facade_default.envWrappers && middleware_insertion_facade_default.envWrappers.length > 0) { - for (const wrapFn of middleware_insertion_facade_default.envWrappers) { - env = wrapFn(env); - } + function wrapExportedHandler(worker) { + if (worker.middleware === void 0 || worker.middleware.length === 0) { + return worker; } - return env; - } - var registeredMiddleware = false; - var facade2 = { - ...middleware_insertion_facade_default.tail && { - tail: maskHandlerEnv(middleware_insertion_facade_default.tail) - }, - ...middleware_insertion_facade_default.trace && { - trace: maskHandlerEnv(middleware_insertion_facade_default.trace) - }, - ...middleware_insertion_facade_default.scheduled && { - scheduled: maskHandlerEnv(middleware_insertion_facade_default.scheduled) - }, - ...middleware_insertion_facade_default.queue && { - queue: maskHandlerEnv(middleware_insertion_facade_default.queue) - }, - ...middleware_insertion_facade_default.test && { - test: maskHandlerEnv(middleware_insertion_facade_default.test) - }, - ...middleware_insertion_facade_default.email && { - email: maskHandlerEnv(middleware_insertion_facade_default.email) - }, - fetch(request, rawEnv, ctx) { - const env = getMaskedEnv(rawEnv); - if (middleware_insertion_facade_default.middleware && middleware_insertion_facade_default.middleware.length > 0) { - if (!registeredMiddleware) { - registeredMiddleware = true; - for (const middleware of middleware_insertion_facade_default.middleware) { - __facade_register__(middleware); - } - } - const __facade_modules_dispatch__ = function(type, init) { - if (type === \\"scheduled\\" && middleware_insertion_facade_default.scheduled !== void 0) { + for (const middleware of worker.middleware) { + __facade_register__(middleware); + } + const fetchDispatcher = function(request, env, ctx) { + if (worker.fetch === void 0) { + throw new Error(\\"Handler does not export a fetch() function.\\"); + } + return worker.fetch(request, env, ctx); + }; + return { + ...worker, + fetch(request, env, ctx) { + const dispatcher = function(type, init) { + if (type === \\"scheduled\\" && worker.scheduled !== void 0) { const controller = new __Facade_ScheduledController__( Date.now(), init.cron ?? \\"\\", () => { } ); - return middleware_insertion_facade_default.scheduled(controller, env, ctx); + return worker.scheduled(controller, env, ctx); } }; + return __facade_invoke__(request, env, ctx, dispatcher, fetchDispatcher); + } + }; + } + function wrapWorkerEntrypoint(klass) { + if (klass.middleware === void 0 || klass.middleware.length === 0) { + return klass; + } + for (const middleware of klass.middleware) { + __facade_register__(middleware); + } + return class extends klass { + #fetchDispatcher = (request, env, ctx) => { + this.env = env; + this.ctx = ctx; + if (super.fetch === void 0) { + throw new Error(\\"Entrypoint class does not define a fetch() function.\\"); + } + return super.fetch(request); + }; + #dispatcher = (type, init) => { + if (type === \\"scheduled\\" && super.scheduled !== void 0) { + const controller = new __Facade_ScheduledController__( + Date.now(), + init.cron ?? \\"\\", + () => { + } + ); + return super.scheduled(controller); + } + }; + fetch(request) { return __facade_invoke__( request, - env, - ctx, - __facade_modules_dispatch__, - __facade_modules_fetch__ + this.env, + this.ctx, + this.#dispatcher, + this.#fetchDispatcher ); - } else { - return __facade_modules_fetch__(request, env, ctx); } - } - }; - function maskHandlerEnv(handler) { - return (data, env, ctx) => handler(data, getMaskedEnv(env), ctx); + }; + } + var WRAPPED_ENTRY; + if (typeof middleware_insertion_facade_default === \\"object\\") { + WRAPPED_ENTRY = wrapExportedHandler(middleware_insertion_facade_default); + } else if (typeof middleware_insertion_facade_default === \\"function\\") { + WRAPPED_ENTRY = wrapWorkerEntrypoint(middleware_insertion_facade_default); } - var middleware_loader_entry_default = facade2; + var middleware_loader_entry_default = WRAPPED_ENTRY; export { - DurableObjectExample2 as DurableObjectExample, + DurableObjectExample, middleware_loader_entry_default as default }; //# sourceMappingURL=index.js.map" diff --git a/packages/wrangler/src/api/dev.ts b/packages/wrangler/src/api/dev.ts index 4fcff0f382b8..9e978622d5b7 100644 --- a/packages/wrangler/src/api/dev.ts +++ b/packages/wrangler/src/api/dev.ts @@ -45,6 +45,7 @@ export interface UnstableDevOptions { binding: string; service: string; environment?: string | undefined; + entrypoint?: string | undefined; }[]; r2?: { binding: string; diff --git a/packages/wrangler/src/api/integrations/platform/index.ts b/packages/wrangler/src/api/integrations/platform/index.ts index 6f25dc42c7ec..071e335e59f3 100644 --- a/packages/wrangler/src/api/integrations/platform/index.ts +++ b/packages/wrangler/src/api/integrations/platform/index.ts @@ -128,6 +128,7 @@ async function getMiniflareOptionsFromConfig( const bindings = getBindings(rawConfig, env, true, {}); const workerDefinitions = await getBoundRegisteredWorkers({ + name: rawConfig.name, services: bindings.services, durableObjects: rawConfig["durable_objects"], }); @@ -137,6 +138,7 @@ async function getMiniflareOptionsFromConfig( bindings, workerDefinitions, queueConsumers: undefined, + services: rawConfig.services, serviceBindings: {}, }); @@ -236,6 +238,7 @@ export function unstable_getMiniflareWorkerOptions(configPath: string): { bindings, workerDefinitions: undefined, queueConsumers: config.queues.consumers, + services: [], serviceBindings: {}, }); diff --git a/packages/wrangler/src/api/startDevWorker/ProxyController.ts b/packages/wrangler/src/api/startDevWorker/ProxyController.ts index c2706aa3aec6..11406eec8a29 100644 --- a/packages/wrangler/src/api/startDevWorker/ProxyController.ts +++ b/packages/wrangler/src/api/startDevWorker/ProxyController.ts @@ -126,8 +126,12 @@ export class ProxyController extends EventEmitter { PROXY_CONTROLLER_AUTH_SECRET: this.secret, }, - unsafeDirectHost: this.latestConfig.dev?.inspector?.hostname, - unsafeDirectPort: this.latestConfig.dev?.inspector?.port ?? 0, + unsafeDirectSockets: [ + { + host: this.latestConfig.dev?.inspector?.hostname, + port: this.latestConfig.dev?.inspector?.port ?? 0, + }, + ], // no need to use file-system, so don't cache: false, diff --git a/packages/wrangler/src/api/startDevWorker/events.ts b/packages/wrangler/src/api/startDevWorker/events.ts index 7e8cca48df70..7c79c053d737 100644 --- a/packages/wrangler/src/api/startDevWorker/events.ts +++ b/packages/wrangler/src/api/startDevWorker/events.ts @@ -1,4 +1,5 @@ import type { CfDurableObject } from "../../deployment-bundle/worker"; +import type { WorkerEntrypointsDefinition } from "../../dev-registry"; import type { EsbuildBundle } from "../../dev/use-esbuild"; import type { DevToolsEvent } from "./devtools"; import type { StartDevWorkerOptions } from "./types"; @@ -149,4 +150,5 @@ export type ProxyData = { liveReload?: boolean; proxyLogsToController?: boolean; internalDurableObjects?: CfDurableObject[]; + entrypointAddresses: WorkerEntrypointsDefinition | undefined; }; diff --git a/packages/wrangler/src/cli.ts b/packages/wrangler/src/cli.ts index 8645665bf714..8eca194953d6 100644 --- a/packages/wrangler/src/cli.ts +++ b/packages/wrangler/src/cli.ts @@ -29,3 +29,4 @@ export type { UnstableDevWorker, UnstableDevOptions }; export * from "./api/integrations"; export { default as unstable_splitSqlQuery } from "./d1/splitter"; +export { startWorkerRegistryServer as unstable_startWorkerRegistryServer } from "./dev-registry"; diff --git a/packages/wrangler/src/config/environment.ts b/packages/wrangler/src/config/environment.ts index 9f25446fcad9..0bf7f1cc0e1f 100644 --- a/packages/wrangler/src/config/environment.ts +++ b/packages/wrangler/src/config/environment.ts @@ -583,6 +583,8 @@ export interface EnvironmentNonInheritable { service: string; /** The environment of the service (e.g. production, staging, etc). */ environment?: string; + /** Optionally, the entrypoint (named export) of the service to bind to. */ + entrypoint?: string; }[] | undefined; diff --git a/packages/wrangler/src/config/index.ts b/packages/wrangler/src/config/index.ts index 0fba6f472a60..28f75b7de8e8 100644 --- a/packages/wrangler/src/config/index.ts +++ b/packages/wrangler/src/config/index.ts @@ -371,10 +371,10 @@ export function printBindings(bindings: CfWorkerInit["bindings"]) { if (services !== undefined && services.length > 0) { output.push({ type: "Services", - entries: services.map(({ binding, service, environment }) => { + entries: services.map(({ binding, service, environment, entrypoint }) => { let value = service; if (environment) { - value += ` - ${environment}`; + value += ` - ${environment}${entrypoint ? ` (#${entrypoint})` : ""}`; } return { diff --git a/packages/wrangler/src/config/validation.ts b/packages/wrangler/src/config/validation.ts index 3126fe210bcc..79e42335ff34 100644 --- a/packages/wrangler/src/config/validation.ts +++ b/packages/wrangler/src/config/validation.ts @@ -2690,7 +2690,7 @@ const validateServiceBinding: ValidatorFn = (diagnostics, field, value) => { return false; } let isValid = true; - // Service bindings must have a binding, service, and environment. + // Service bindings must have a binding, a service, optionally an environment, and, optionally an entrypoint. if (!isRequiredProperty(value, "binding", "string")) { diagnostics.errors.push( `"${field}" bindings should have a string "binding" field but got ${JSON.stringify( @@ -2715,6 +2715,14 @@ const validateServiceBinding: ValidatorFn = (diagnostics, field, value) => { ); isValid = false; } + if (!isOptionalProperty(value, "entrypoint", "string")) { + diagnostics.errors.push( + `"${field}" bindings should have a string "entrypoint" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } return isValid; }; diff --git a/packages/wrangler/src/deploy/deploy.ts b/packages/wrangler/src/deploy/deploy.ts index eff99f25ca8a..6eb4e9d96744 100644 --- a/packages/wrangler/src/deploy/deploy.ts +++ b/packages/wrangler/src/deploy/deploy.ts @@ -512,11 +512,6 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m assets: config.assets, // enable the cache when publishing bypassAssetCache: false, - services: config.services, - // We don't set workerDefinitions here, - // because we don't want to apply the dev-time - // facades on top of it - workerDefinitions: undefined, // We want to know if the build is for development or publishing // This could potentially cause issues as we no longer have identical behaviour between dev and deploy? targetConsumer: "deploy", diff --git a/packages/wrangler/src/deployment-bundle/apply-middleware.ts b/packages/wrangler/src/deployment-bundle/apply-middleware.ts index 8101e7cce0b1..2ee6d0cdc8af 100644 --- a/packages/wrangler/src/deployment-bundle/apply-middleware.ts +++ b/packages/wrangler/src/deployment-bundle/apply-middleware.ts @@ -2,7 +2,6 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { getBasePath } from "../paths"; import { dedent } from "../utils/dedent"; -import type { DurableObjectBindings } from "../config/environment"; import type { Entry } from "./entry"; import type { CfScriptFormat } from "./worker"; @@ -25,8 +24,7 @@ export interface MiddlewareLoader { export async function applyMiddlewareLoaderFacade( entry: Entry, tmpDirPath: string, - middleware: MiddlewareLoader[], - doBindings: DurableObjectBindings + middleware: MiddlewareLoader[] ): Promise<{ entry: Entry; inject?: string[] }> { // Firstly we need to insert the middleware array into the project, // and then we load the middleware - this insertion and loading is @@ -59,48 +57,19 @@ export async function applyMiddlewareLoaderFacade( const middlewareFns = middlewareIdentifiers.map(([m]) => `${m}.default`); if (entry.format === "modules") { - const middlewareWrappers = middlewareIdentifiers - .map(([m]) => `${m}.wrap`) - .join(","); - - const durableObjects = doBindings - // Don't shim anything not local to this worker - .filter((b) => !b.script_name) - // Reexport the DO classnames - .map( - (b) => - /*javascript*/ `export const ${b.class_name} = maskDurableObjectDefinition(OTHER_EXPORTS.${b.class_name});` - ) - .join("\n"); await fs.promises.writeFile( dynamicFacadePath, dedent/*javascript*/ ` import worker, * as OTHER_EXPORTS from "${prepareFilePath(entry.file)}"; ${imports} - const envWrappers = [${middlewareWrappers}].filter(Boolean); - const facade = { - ...worker, - envWrappers, - middleware: [ - ${middlewareFns.join(",")}, - ...(worker.middleware ? worker.middleware : []), - ].filter(Boolean) - } + + worker.middleware = [ + ${middlewareFns.join(",")}, + ...(worker.middleware ?? []), + ].filter(Boolean); + export * from "${prepareFilePath(entry.file)}"; - - const maskDurableObjectDefinition = (cls) => - class extends cls { - constructor(state, env) { - let wrappedEnv = env - for (const wrapFn of envWrappers) { - wrappedEnv = wrapFn(wrappedEnv) - } - super(state, wrappedEnv); - } - }; - ${durableObjects} - - export default facade; + export default worker; ` ); diff --git a/packages/wrangler/src/deployment-bundle/bundle.ts b/packages/wrangler/src/deployment-bundle/bundle.ts index 5b39587a0829..5e7950f2a0aa 100644 --- a/packages/wrangler/src/deployment-bundle/bundle.ts +++ b/packages/wrangler/src/deployment-bundle/bundle.ts @@ -20,7 +20,6 @@ import { writeAdditionalModules } from "./find-additional-modules"; import { noopModuleCollector } from "./module-collection"; import type { Config } from "../config"; import type { DurableObjectBindings } from "../config/environment"; -import type { WorkerRegistry } from "../dev-registry"; import type { MiddlewareLoader } from "./apply-middleware"; import type { Entry } from "./entry"; import type { ModuleCollector } from "./module-collection"; @@ -75,8 +74,6 @@ export type BundleOptions = { nodejsCompat?: boolean; define: Config["define"]; checkFetch: boolean; - services?: Config["services"]; - workerDefinitions?: WorkerRegistry; targetConsumer: "dev" | "deploy"; testScheduled?: boolean; inject?: string[]; @@ -114,8 +111,6 @@ export async function bundleWorker( checkFetch, assets, bypassAssetCache, - workerDefinitions, - services, targetConsumer, testScheduled, inject: injectOption, @@ -201,30 +196,6 @@ export async function bundleWorker( }); } - if ( - targetConsumer === "dev" && - !!( - workerDefinitions && - Object.keys(workerDefinitions).length > 0 && - services && - services.length > 0 - ) - ) { - middlewareToLoad.push({ - name: "multiworker-dev", - path: "templates/middleware/middleware-multiworker-dev.ts", - config: { - workers: Object.fromEntries( - (services || []).map((serviceBinding) => [ - serviceBinding.binding, - workerDefinitions?.[serviceBinding.service] || null, - ]) - ), - }, - supports: ["modules"], - }); - } - // If using watch, build result will not be returned. // This plugin will retrieve the build result on the first build. let initialBuildResult: (result: esbuild.BuildResult) => void; @@ -275,8 +246,7 @@ export async function bundleWorker( const result = await applyMiddlewareLoaderFacade( entry, tmpDir.path, - middlewareToLoad, - doBindings + middlewareToLoad ); entry = result.entry; diff --git a/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts b/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts index 263fca354d39..b351f6da207d 100644 --- a/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts +++ b/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts @@ -75,7 +75,13 @@ export type WorkerMetadataBinding = } | { type: "constellation"; name: string; project: string } | { type: "hyperdrive"; name: string; id: string } - | { type: "service"; name: string; service: string; environment?: string } + | { + type: "service"; + name: string; + service: string; + environment?: string; + entrypoint?: string; + } | { type: "analytics_engine"; name: string; dataset?: string } | { type: "dispatch_namespace"; @@ -244,14 +250,17 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData { }); }); - bindings.services?.forEach(({ binding, service, environment }) => { - metadataBindings.push({ - name: binding, - type: "service", - service, - ...(environment && { environment }), - }); - }); + bindings.services?.forEach( + ({ binding, service, environment, entrypoint }) => { + metadataBindings.push({ + name: binding, + type: "service", + service, + ...(environment && { environment }), + ...(entrypoint && { entrypoint }), + }); + } + ); bindings.analytics_engine_datasets?.forEach(({ binding, dataset }) => { metadataBindings.push({ diff --git a/packages/wrangler/src/deployment-bundle/worker.ts b/packages/wrangler/src/deployment-bundle/worker.ts index ecd9c14d8c49..6979a9c398ff 100644 --- a/packages/wrangler/src/deployment-bundle/worker.ts +++ b/packages/wrangler/src/deployment-bundle/worker.ts @@ -181,6 +181,7 @@ interface CfService { binding: string; service: string; environment?: string; + entrypoint?: string; } interface CfAnalyticsEngineDataset { diff --git a/packages/wrangler/src/dev-registry.ts b/packages/wrangler/src/dev-registry.ts index 9c950a497b5c..4c9b9bdb2186 100644 --- a/packages/wrangler/src/dev-registry.ts +++ b/packages/wrangler/src/dev-registry.ts @@ -1,5 +1,6 @@ -import net from "net"; +import events from "node:events"; import { createServer } from "node:http"; +import net from "node:net"; import bodyParser from "body-parser"; import express from "express"; import { createHttpTerminator } from "http-terminator"; @@ -9,20 +10,29 @@ import type { Config } from "./config"; import type { HttpTerminator } from "http-terminator"; import type { Server } from "node:http"; -const DEV_REGISTRY_PORT = 6284; +// Safety of `!`: `parseInt(undefined)` is NaN +// eslint-disable-next-line @typescript-eslint/no-non-null-assertion +let DEV_REGISTRY_PORT = parseInt(process.env.WRANGLER_WORKER_REGISTRY_PORT!); +if (Number.isNaN(DEV_REGISTRY_PORT)) DEV_REGISTRY_PORT = 6284; const DEV_REGISTRY_HOST = `http://127.0.0.1:${DEV_REGISTRY_PORT}`; -let server: Server | null; -let terminator: HttpTerminator; +let globalServer: Server | null; +let globalTerminator: HttpTerminator; export type WorkerRegistry = Record; +export type WorkerEntrypointsDefinition = Record< + /* name */ "default" | string, + { host: string; port: number } | undefined +>; + export type WorkerDefinition = { port: number | undefined; protocol: "http" | "https" | undefined; host: string | undefined; mode: "local" | "remote"; headers?: Record; + entrypointAddresses?: WorkerEntrypointsDefinition; durableObjects: { name: string; className: string }[]; durableObjectsHost?: string; durableObjectsPort?: number; @@ -53,42 +63,54 @@ async function isPortAvailable() { const jsonBodyParser = bodyParser.json(); +export async function startWorkerRegistryServer(port: number) { + const app = express(); + + let workers: WorkerRegistry = {}; + app + .get("/workers", async (req, res) => { + res.json(workers); + }) + .post("/workers/:workerId", jsonBodyParser, async (req, res) => { + workers[req.params.workerId] = req.body; + res.json(null); + }) + .delete(`/workers/:workerId`, async (req, res) => { + delete workers[req.params.workerId]; + res.json(null); + }) + .delete("/workers", async (req, res) => { + workers = {}; + res.json(null); + }); + + const appServer = createServer(app); + const appTerminator = createHttpTerminator({ server: appServer }); + + const listeningPromise = events.once(appServer, "listening"); + appServer.listen(port, "127.0.0.1"); + await listeningPromise; + + return { server: appServer, terminator: appTerminator }; +} + /** * Start the service registry. It's a simple server * that exposes endpoints for registering and unregistering * services, as well as getting the state of the registry. */ export async function startWorkerRegistry() { - if ((await isPortAvailable()) && !server) { - const app = express(); - - let workers: WorkerRegistry = {}; - app - .get("/workers", async (req, res) => { - res.json(workers); - }) - .post("/workers/:workerId", jsonBodyParser, async (req, res) => { - workers[req.params.workerId] = req.body; - res.json(null); - }) - .delete(`/workers/:workerId`, async (req, res) => { - delete workers[req.params.workerId]; - res.json(null); - }) - .delete("/workers", async (req, res) => { - workers = {}; - res.json(null); - }); - server = createServer(app); - terminator = createHttpTerminator({ server }); - server.listen(DEV_REGISTRY_PORT, "127.0.0.1"); + if ((await isPortAvailable()) && !globalServer) { + const result = await startWorkerRegistryServer(DEV_REGISTRY_PORT); + globalServer = result.server; + globalTerminator = result.terminator; /** * The registry server may have already been started by another wrangler process. * If wrangler processes are run in parallel, isPortAvailable() can return true * while another process spins up the server */ - server.once("error", (err) => { + globalServer.once("error", (err) => { if ((err as unknown as { code: string }).code !== "EADDRINUSE") { throw err; } @@ -97,8 +119,8 @@ export async function startWorkerRegistry() { /** * The registry server may close. Reset the server to null for restart. */ - server.on("close", () => { - server = null; + globalServer.on("close", () => { + globalServer = null; }); } } @@ -107,8 +129,8 @@ export async function startWorkerRegistry() { * Stop the service registry. */ export async function stopWorkerRegistry() { - await terminator?.terminate(); - server = null; + await globalTerminator?.terminate(); + globalServer = null; } /** @@ -188,9 +210,11 @@ export async function getRegisteredWorkers(): Promise< * list of the running workers that we're bound to */ export async function getBoundRegisteredWorkers({ + name, services, durableObjects, }: { + name: string | undefined; services: Config["services"] | undefined; durableObjects: Config["durable_objects"] | undefined; }) { @@ -205,7 +229,8 @@ export async function getBoundRegisteredWorkers({ const filteredWorkers = Object.fromEntries( Object.entries(workerDefinitions || {}).filter( ([key, _value]) => - serviceNames.includes(key) || durableObjectServices.includes(key) + key !== name && // Always exclude current worker to avoid infinite loops + (serviceNames.includes(key) || durableObjectServices.includes(key)) ) ); return filteredWorkers; diff --git a/packages/wrangler/src/dev.tsx b/packages/wrangler/src/dev.tsx index c525d454f580..ce267fb19704 100644 --- a/packages/wrangler/src/dev.tsx +++ b/packages/wrangler/src/dev.tsx @@ -339,6 +339,7 @@ export type AdditionalDevProps = { binding: string; service: string; environment?: string; + entrypoint?: string; }[]; r2?: { binding: string; @@ -753,7 +754,7 @@ async function validateDevServerSettings( (service) => `${service.binding} (${service.service}${ service.environment ? `@${service.environment}` : "" - })` + }${service.entrypoint ? `#${service.entrypoint}` : ""})` ) .join(", ")}` ); diff --git a/packages/wrangler/src/dev/dev.tsx b/packages/wrangler/src/dev/dev.tsx index 5460a3472947..b6a72cad7228 100644 --- a/packages/wrangler/src/dev/dev.tsx +++ b/packages/wrangler/src/dev/dev.tsx @@ -72,6 +72,7 @@ function useDevRegistry( mode === "local" ? setInterval(() => { getBoundRegisteredWorkers({ + name, services, durableObjects, }).then( @@ -414,7 +415,8 @@ function DevSession(props: DevSessionProps) { await maybeRegisterLocalWorker( url, props.name, - proxyData.internalDurableObjects + proxyData.internalDurableObjects, + proxyData.entrypointAddresses ); } @@ -471,6 +473,7 @@ function DevSession(props: DevSessionProps) { onReady={announceAndOnReady} enablePagesAssetsServiceBinding={props.enablePagesAssetsServiceBinding} sourceMapPath={bundle?.sourceMapPath} + services={props.bindings.services} /> ) : ( { + throw new Error(\`Cannot access \\\`\${className}#\${key}\\\` as Durable Object RPC is not yet supported between multiple \\\`wrangler dev\\\` sessions.\`); + }); + + // Forward regular HTTP requests to the other "wrangler dev" session + klass.prototype.fetch = function(request) { + if (proxyUrl === undefined) { + return new Response(\`[wrangler] Couldn't find \\\`wrangler dev\\\` session for class "\${className}" to proxy to\`, { status: 503 }); + } + const proxyRequest = new Request(proxyUrl, request); + proxyRequest.headers.set(HEADER_URL, request.url); + proxyRequest.headers.set(HEADER_NAME, className); + proxyRequest.headers.set(HEADER_ID, this.ctx.id.toString()); + proxyRequest.headers.set(HEADER_CF_BLOB, JSON.stringify(request.cf)); + return fetch(proxyRequest); + }; + + return klass; +} + +function createNotFoundWorkerEntrypointClass({ service }) { + const klass = createProxyPrototypeClass(WorkerEntrypoint, (key) => { + throw new Error(\`Cannot access \\\`\${key}\\\` as we couldn't find a \\\`wrangler dev\\\` session for service "\${service}" to proxy to.\`); + }); + + // Return regular HTTP response for HTTP requests + klass.prototype.fetch = function(request) { + const message = \`[wrangler] Couldn't find \\\`wrangler dev\\\` session for service "\${service}" to proxy to\`; + return new Response(message, { status: 503 }); + }; + + return klass; } export default { @@ -76,6 +147,7 @@ export default { const originalUrl = request.headers.get(HEADER_URL); const className = request.headers.get(HEADER_NAME); const idString = request.headers.get(HEADER_ID); + const cfBlobString = request.headers.get(HEADER_CF_BLOB); if (originalUrl === null || className === null || idString === null) { return new Response("[wrangler] Received Durable Object proxy request with missing headers", { status: 400 }); } @@ -83,10 +155,11 @@ export default { request.headers.delete(HEADER_URL); request.headers.delete(HEADER_NAME); request.headers.delete(HEADER_ID); + request.headers.delete(HEADER_CF_BLOB); const ns = env[className]; const id = ns.idFromString(idString); const stub = ns.get(id); - return stub.fetch(request); + return stub.fetch(request, { cf: JSON.parse(cfBlobString ?? "{}") }); } } `; @@ -121,6 +194,7 @@ export interface ConfigBundle { localUpstream: string | undefined; upstreamProtocol: "http" | "https"; inspect: boolean; + services: Config["services"] | undefined; serviceBindings: Record Promise>; } @@ -129,7 +203,7 @@ export class WranglerLog extends Log { log(message: string) { // Hide request logs for external Durable Objects proxy worker - if (message.includes(EXTERNAL_DURABLE_OBJECTS_WORKER_NAME)) return; + if (message.includes(EXTERNAL_SERVICE_WORKER_NAME)) return; super.log(message); } @@ -180,21 +254,34 @@ function buildLog(): Log { return new WranglerLog(level, { prefix: "wrangler-UserWorker" }); } +async function getEntrypointNames(entrypointSource: string) { + await esmLexer.init; + const [_imports, exports] = esmLexer.parse(entrypointSource); + // TODO(soon): support `export * from "...";` with `--no-bundle`. Without + // `--no-bundle`, `esbuild` will bundle these, so they'll be picked up here. + return exports.map(({ n }) => n); +} + async function buildSourceOptions( config: ConfigBundle -): Promise { +): Promise<{ sourceOptions: SourceOptions; entrypointNames: string[] }> { const scriptPath = realpathSync(config.bundle.path); if (config.format === "modules") { - const modulesRoot = path.dirname(scriptPath); - const { entrypointSource, modules } = - config.bundle.type === "python" - ? { - entrypointSource: readFileSync(scriptPath, "utf8"), - modules: config.bundle.modules, - } - : withSourceURLs(scriptPath, config.bundle.modules); + const isPython = config.bundle.type === "python"; - return { + const { entrypointSource, modules } = isPython + ? { + entrypointSource: readFileSync(scriptPath, "utf8"), + modules: config.bundle.modules, + } + : withSourceURLs(scriptPath, config.bundle.modules); + + const entrypointNames = isPython + ? [] + : await getEntrypointNames(entrypointSource); + + const modulesRoot = path.dirname(scriptPath); + const sourceOptions: SourceOptions = { modulesRoot, modules: [ @@ -212,9 +299,10 @@ async function buildSourceOptions( })), ], }; + return { sourceOptions, entrypointNames }; } else { // Miniflare will handle adding `//# sourceURL` comments if they're missing - return { scriptPath }; + return { sourceOptions: { scriptPath }, entrypointNames: [] }; } } @@ -270,6 +358,7 @@ type MiniflareBindingsConfig = Pick< | "workerDefinitions" | "queueConsumers" | "name" + | "services" | "serviceBindings" > & Partial>; @@ -303,6 +392,89 @@ export function buildMiniflareBindingOptions(config: MiniflareBindingsConfig): { } } + // Setup service bindings to external services + const serviceBindings: NonNullable = { + ...config.serviceBindings, + }; + const notFoundServices = new Set(); + for (const service of config.services ?? []) { + if (service.service === config.name) { + // If this is a service binding to the current worker, don't bother using + // the dev registry to look up the address, just bind to it directly. + serviceBindings[service.binding] = { + name: getName(config), + entrypoint: service.entrypoint, + }; + continue; + } + + const target = config.workerDefinitions?.[service.service]; + if (target?.host === undefined || target.port === undefined) { + // If the target isn't in the registry, always return an error response + notFoundServices.add(service.service); + serviceBindings[service.binding] = { + name: EXTERNAL_SERVICE_WORKER_NAME, + entrypoint: getIdentifier(`service_${service.service}`), + }; + } else { + // Otherwise, try to build an `external` service to it. `external` + // services support JSRPC over HTTP CONNECT using a special hostname. + // Refer to https://github.com/cloudflare/workerd/pull/1757 for details. + let address: `${string}:${number}`; + if (service.entrypoint !== undefined) { + // If the user has requested a named entrypoint... + if (target.entrypointAddresses === undefined) { + // ...but the "server" `wrangler` hasn't provided any because it's too + // old, throw. + throw new UserError( + `The \`wrangler dev\` session for service "${service.service}" does not support proxying entrypoints. Please upgrade "${service.service}"'s \`wrangler\` version.` + ); + } + const entrypointAddress = + target.entrypointAddresses[service.entrypoint]; + if (entrypointAddress === undefined) { + // ...but the named entrypoint doesn't exist, throw + throw new UserError( + `The \`wrangler dev\` session for service "${service.service}" does not export an entrypoint named "${service.entrypoint}"` + ); + } + address = `${entrypointAddress.host}:${entrypointAddress.port}`; + } else { + // Otherwise, if the user hasn't specified a named entrypoint, assume + // they meant to bind to the `default` entrypoint. + const defaultEntrypointAddress = + target.entrypointAddresses?.["default"]; + if (defaultEntrypointAddress === undefined) { + // If the "server" `wrangler` is too old to provide direct entrypoint + // addresses, fallback to sending requests directly to the target... + if (target.protocol === "https") { + // ...unless the target is listening on HTTPS, in which case throw. + // We can't support this as `workerd` requires us to explicitly + // configure allowed self-signed certificates. These aren't stored + // in the registry. There's no blanket `rejectUnauthorized: false` + // option like in Node. + throw new UserError( + `Cannot proxy to \`wrangler dev\` session for service "${service.service}" because it uses HTTPS. Please upgrade "${service.service}"'s \`wrangler\` version, or remove the \`--local-protocol\`/\`dev.local_protocol\` option.` + ); + } + address = `${target.host}:${target.port}`; + } else { + address = `${defaultEntrypointAddress.host}:${defaultEntrypointAddress.port}`; + } + } + + serviceBindings[service.binding] = { + external: { + address, + http: { + style: HttpOptions_Style.PROXY, + cfBlobHeader: CoreHeaders.CF_BLOB, + }, + }, + }; + } + } + // Partition Durable Objects based on whether they're internal (defined by // this session's worker), or external (defined by another session's worker // registered in the dev registry) @@ -316,7 +488,7 @@ export function buildMiniflareBindingOptions(config: MiniflareBindingsConfig): { } // Setup Durable Object bindings and proxy worker externalWorkers.push({ - name: EXTERNAL_DURABLE_OBJECTS_WORKER_NAME, + name: EXTERNAL_SERVICE_WORKER_NAME, // Bind all internal objects, so they're accessible by all other sessions // that proxy requests for our objects to this worker durableObjects: Object.fromEntries( @@ -326,15 +498,16 @@ export function buildMiniflareBindingOptions(config: MiniflareBindingsConfig): { ]) ), // Use this worker instead of the user worker if the pathname is - // `/${EXTERNAL_DURABLE_OBJECTS_WORKER_NAME}` - routes: [`*/${EXTERNAL_DURABLE_OBJECTS_WORKER_NAME}`], + // `/${EXTERNAL_SERVICE_WORKER_NAME}` + routes: [`*/${EXTERNAL_SERVICE_WORKER_NAME}`], // Use in-memory storage for the stub object classes *declared* by this // script. They don't need to persist anything, and would end up using the // incorrect unsafe unique key. unsafeEphemeralDurableObjects: true, + compatibilityDate: "2024-01-01", modules: true, script: - EXTERNAL_DURABLE_OBJECTS_WORKER_SCRIPT + + EXTERNAL_SERVICE_WORKER_SCRIPT + // Add stub object classes that proxy requests to the correct session externalObjects .map(({ class_name, script_name }) => { @@ -344,7 +517,7 @@ export function buildMiniflareBindingOptions(config: MiniflareBindingsConfig): { ({ className }) => className === class_name ); - const identifier = getIdentifier(`${script_name}_${class_name}`); + const identifier = getIdentifier(`do_${script_name}_${class_name}`); const classNameJson = JSON.stringify(class_name); if ( target?.host === undefined || @@ -353,15 +526,26 @@ export function buildMiniflareBindingOptions(config: MiniflareBindingsConfig): { ) { // If we couldn't find the target or the class, create a stub object // that just returns `503 Service Unavailable` responses. - return `export const ${identifier} = createClass({ className: ${classNameJson} });`; + return `export const ${identifier} = createDurableObjectClass({ className: ${classNameJson} });`; + } else if (target.protocol === "https") { + throw new UserError( + `Cannot proxy to \`wrangler dev\` session for class ${classNameJson} because it uses HTTPS. Please remove the \`--local-protocol\`/\`dev.local_protocol\` option.` + ); } else { // Otherwise, create a stub object that proxies request to the // target session at `${hostname}:${port}`. - const proxyUrl = `http://${target.host}:${target.port}/${EXTERNAL_DURABLE_OBJECTS_WORKER_NAME}`; + const proxyUrl = `http://${target.host}:${target.port}/${EXTERNAL_SERVICE_WORKER_NAME}`; const proxyUrlJson = JSON.stringify(proxyUrl); - return `export const ${identifier} = createClass({ className: ${classNameJson}, proxyUrl: ${proxyUrlJson} });`; + return `export const ${identifier} = createDurableObjectClass({ className: ${classNameJson}, proxyUrl: ${proxyUrlJson} });`; } }) + .join("\n") + + Array.from(notFoundServices) + .map((service) => { + const identifier = getIdentifier(`service_${service}`); + const serviceJson = JSON.stringify(service); + return `export const ${identifier} = createNotFoundWorkerEntrypointClass({ service: ${serviceJson} });`; + }) .join("\n"), }); @@ -420,12 +604,12 @@ export function buildMiniflareBindingOptions(config: MiniflareBindingsConfig): { durableObjects: Object.fromEntries([ ...internalObjects.map(({ name, class_name }) => [name, class_name]), ...externalObjects.map(({ name, class_name, script_name }) => { - const identifier = getIdentifier(`${script_name}_${class_name}`); + const identifier = getIdentifier(`do_${script_name}_${class_name}`); return [ name, { className: identifier, - scriptName: EXTERNAL_DURABLE_OBJECTS_WORKER_NAME, + scriptName: EXTERNAL_SERVICE_WORKER_NAME, // Matches the unique key Miniflare will generate for this object in // the target session. We need to do this so workerd generates the // same IDs it would if this were part of the same process. workerd @@ -443,10 +627,8 @@ export function buildMiniflareBindingOptions(config: MiniflareBindingsConfig): { .map(ratelimitEntry) ?? [] ), - serviceBindings: config.serviceBindings, - + serviceBindings, wrappedBindings: wrappedBindings, - // TODO: check multi worker service bindings also supported }; return { @@ -595,7 +777,11 @@ async function buildMiniflareOptions( log: Log, config: ConfigBundle, proxyToUserWorkerAuthenticationSecret: UUID -): Promise<{ options: MiniflareOptions; internalObjects: CfDurableObject[] }> { +): Promise<{ + options: MiniflareOptions; + internalObjects: CfDurableObject[]; + entrypointNames: string[]; +}> { if (config.crons.length > 0) { logger.warn("Miniflare 3 does not support CRON triggers yet, ignoring..."); } @@ -618,7 +804,7 @@ async function buildMiniflareOptions( ? `${config.upstreamProtocol}://${config.localUpstream}` : undefined; - const sourceOptions = await buildSourceOptions(config); + const { sourceOptions, entrypointNames } = await buildSourceOptions(config); const { bindingOptions, internalObjects, externalWorkers } = buildMiniflareBindingOptions(config); const sitesOptions = buildSitesOptions(config); @@ -659,27 +845,38 @@ async function buildMiniflareOptions( ...sourceOptions, ...bindingOptions, ...sitesOptions, + + // Allow each entrypoint to be accessed directly over `127.0.0.1:0` + unsafeDirectSockets: entrypointNames.map((name) => ({ + host: "127.0.0.1", + port: 0, + entrypoint: name, + proxy: true, + })), }, ...externalWorkers, ], }; - return { options, internalObjects }; + return { options, internalObjects, entrypointNames }; } export interface ReloadedEventOptions { url: URL; internalDurableObjects: CfDurableObject[]; + entrypointAddresses: WorkerEntrypointsDefinition; proxyToUserWorkerAuthenticationSecret: UUID; } export class ReloadedEvent extends Event implements ReloadedEventOptions { readonly url: URL; readonly internalDurableObjects: CfDurableObject[]; + readonly entrypointAddresses: WorkerEntrypointsDefinition; readonly proxyToUserWorkerAuthenticationSecret: UUID; constructor(type: "reloaded", options: ReloadedEventOptions) { super(type); this.url = options.url; this.internalDurableObjects = options.internalDurableObjects; + this.entrypointAddresses = options.entrypointAddresses; this.proxyToUserWorkerAuthenticationSecret = options.proxyToUserWorkerAuthenticationSecret; } @@ -717,11 +914,12 @@ export class MiniflareServer extends TypedEventTarget { async #onBundleUpdate(config: ConfigBundle, opts?: Abortable): Promise { if (opts?.signal?.aborted) return; try { - const { options, internalObjects } = await buildMiniflareOptions( - this.#log, - config, - this.#proxyToUserWorkerAuthenticationSecret - ); + const { options, internalObjects, entrypointNames } = + await buildMiniflareOptions( + this.#log, + config, + this.#proxyToUserWorkerAuthenticationSecret + ); if (opts?.signal?.aborted) return; if (this.#mf === undefined) { this.#mf = new Miniflare(options); @@ -729,12 +927,23 @@ export class MiniflareServer extends TypedEventTarget { await this.#mf.setOptions(options); } const url = await this.#mf.ready; + + // Get entrypoint addresses + const entrypointAddresses: WorkerEntrypointsDefinition = {}; + for (const name of entrypointNames) { + const directUrl = await this.#mf.unsafeGetDirectURL(undefined, name); + const port = parseInt(directUrl.port); + entrypointAddresses[name] = { host: directUrl.hostname, port }; + } + if (opts?.signal?.aborted) return; + const event = new ReloadedEvent("reloaded", { url, internalDurableObjects: internalObjects, proxyToUserWorkerAuthenticationSecret: this.#proxyToUserWorkerAuthenticationSecret, + entrypointAddresses, }); this.dispatchEvent(event); } catch (error: unknown) { diff --git a/packages/wrangler/src/dev/remote.tsx b/packages/wrangler/src/dev/remote.tsx index 8b5e0f88aa62..d0d48a485532 100644 --- a/packages/wrangler/src/dev/remote.tsx +++ b/packages/wrangler/src/dev/remote.tsx @@ -343,6 +343,7 @@ export function useWorker( }, liveReload: false, // liveReload currently disabled in remote-mode, but will be supported with startDevWorker proxyLogsToController: true, + entrypointAddresses: undefined, }; onReady?.( @@ -464,6 +465,7 @@ export async function startRemoteServer(props: RemoteProps) { }, liveReload: false, // liveReload currently disabled in remote-mode, but will be supported with startDevWorker proxyLogsToController: true, + entrypointAddresses: undefined, }; props.onReady?.(ip, port, proxyData); diff --git a/packages/wrangler/src/dev/start-server.ts b/packages/wrangler/src/dev/start-server.ts index a7412a76dd02..091a87198000 100644 --- a/packages/wrangler/src/dev/start-server.ts +++ b/packages/wrangler/src/dev/start-server.ts @@ -67,6 +67,7 @@ export async function startDevServer( await startWorkerRegistry(); if (props.local) { const boundRegisteredWorkers = await getBoundRegisteredWorkers({ + name: props.name, services: props.bindings.services, durableObjects: props.bindings.durable_objects, }); @@ -136,8 +137,6 @@ export async function startDevServer( noBundle: props.noBundle, findAdditionalModules: props.findAdditionalModules, assets: props.assetsConfig, - workerDefinitions, - services: props.bindings.services, testScheduled: props.testScheduled, local: props.local, doBindings: props.bindings.durable_objects?.bindings ?? [], @@ -190,7 +189,8 @@ export async function startDevServer( await maybeRegisterLocalWorker( url, props.name, - proxyData.internalDurableObjects + proxyData.internalDurableObjects, + proxyData.entrypointAddresses ); props.onReady?.(ip, port, proxyData); @@ -207,6 +207,7 @@ export async function startDevServer( usageModel: props.usageModel, workerDefinitions, sourceMapPath: bundle?.sourceMapPath, + services: props.bindings.services, }); return { @@ -294,8 +295,6 @@ async function runEsbuild({ define, noBundle, findAdditionalModules, - workerDefinitions, - services, testScheduled, local, doBindings, @@ -311,7 +310,6 @@ async function runEsbuild({ rules: Config["rules"]; assets: Config["assets"]; define: Config["define"]; - services: Config["services"]; serveAssetsFromWorker: boolean; tsconfig: string | undefined; minify: boolean | undefined; @@ -319,7 +317,6 @@ async function runEsbuild({ nodejsCompat: boolean | undefined; noBundle: boolean; findAdditionalModules: boolean | undefined; - workerDefinitions: WorkerRegistry; testScheduled?: boolean; local: boolean; doBindings: DurableObjectBindings; @@ -364,8 +361,6 @@ async function runEsbuild({ assets, // disable the cache in dev bypassAssetCache: true, - workerDefinitions, - services, targetConsumer: "dev", // We are starting a dev server local, testScheduled, @@ -439,6 +434,7 @@ export async function startLocalServer( // workers written in "service-worker" format still need to proxy logs to the ProxyController proxyLogsToController: props.format === "service-worker", internalDurableObjects: event.internalDurableObjects, + entrypointAddresses: event.entrypointAddresses, }; props.onReady?.(event.url.hostname, parseInt(event.url.port), proxyData); diff --git a/packages/wrangler/src/dev/use-esbuild.ts b/packages/wrangler/src/dev/use-esbuild.ts index 75dfad47f8c0..50af177e5dfc 100644 --- a/packages/wrangler/src/dev/use-esbuild.ts +++ b/packages/wrangler/src/dev/use-esbuild.ts @@ -185,8 +185,6 @@ export function useEsbuild({ assets, // disable the cache in dev bypassAssetCache: true, - workerDefinitions, - services, targetConsumer, testScheduled, plugins: [onEnd], diff --git a/packages/wrangler/src/init.ts b/packages/wrangler/src/init.ts index 8dfdb147bd17..c58d1371dc11 100644 --- a/packages/wrangler/src/init.ts +++ b/packages/wrangler/src/init.ts @@ -1070,6 +1070,7 @@ export function mapBindings(bindings: WorkerMetadataBinding[]): RawConfig { binding: binding.name, service: binding.service, environment: binding.environment, + entrypoint: binding.entrypoint, }, ]; } diff --git a/packages/wrangler/src/pages/dev.ts b/packages/wrangler/src/pages/dev.ts index 642c2899ab8f..c5567c9d37af 100644 --- a/packages/wrangler/src/pages/dev.ts +++ b/packages/wrangler/src/pages/dev.ts @@ -67,11 +67,13 @@ const BINDING_REGEXP = new RegExp(/^(?[^=]+)(?:=(?[^\s]+))?$/); /* SERVICE_BINDING_REGEXP matches strings like: * - "binding=service" * - "binding=service@environment" + * - "binding=service#entrypoint" * This is used to capture both the binding name (how the binding is used in JS) alongside the name of the service it needs to bind to. * Additionally it can also accept an environment which indicates what environment the service has to be running for. + * Additionally it can also accept an entrypoint which indicates what named entrypoint of the service to use, if not the default. */ const SERVICE_BINDING_REGEXP = new RegExp( - /^(?[^=]+)=(?[^@\s]+)(@(?.*)$)?$/ + /^(?[^=]+)=(?[^@#\s]+)(@(?.*)$)?(#(?.*))?$/ ); const DEFAULT_IP = process.platform === "win32" ? "127.0.0.1" : "localhost"; @@ -517,7 +519,7 @@ export const Handler = async (args: PagesDevArguments) => { ); } - let entrypoint = scriptPath; + let scriptEntrypoint = scriptPath; // custom _routes.json apply only to Functions or Advanced Mode Pages projects if ( @@ -562,11 +564,11 @@ export const Handler = async (args: PagesDevArguments) => { routesJSONContents = readFileSync(routesJSONPath, "utf-8"); validateRoutes(JSON.parse(routesJSONContents), directory); - entrypoint = join( + scriptEntrypoint = join( getPagesTmpDir(), `${Math.random().toString(36).slice(2)}.js` ); - await runBuild(scriptPath, entrypoint, routesJSONContents); + await runBuild(scriptPath, scriptEntrypoint, routesJSONContents); } catch (err) { if (err instanceof FatalError) { throw err; @@ -592,7 +594,7 @@ export const Handler = async (args: PagesDevArguments) => { */ routesJSONContents = readFileSync(routesJSONPath, "utf-8"); validateRoutes(JSON.parse(routesJSONContents), directory as string); - await runBuild(scriptPath, entrypoint, routesJSONContents); + await runBuild(scriptPath, scriptEntrypoint, routesJSONContents); } catch (err) { /** * If _routes.json is invalid, don't exit but instead fallback to a sensible default @@ -621,13 +623,13 @@ export const Handler = async (args: PagesDevArguments) => { ); routesJSONContents = JSON.stringify(defaultRoutesJSONSpec); - await runBuild(scriptPath, entrypoint, routesJSONContents); + await runBuild(scriptPath, scriptEntrypoint, routesJSONContents); } }); } } - const { stop, waitUntilExit } = await unstable_dev(entrypoint, { + const { stop, waitUntilExit } = await unstable_dev(scriptEntrypoint, { env: undefined, ip, port, @@ -985,7 +987,7 @@ function getBindingsFromArgs(args: PagesDevArguments): Partial< if (args.service?.length) { services = args.service .map((serviceBinding) => { - const { binding, service, environment } = + const { binding, service, environment, entrypoint } = SERVICE_BINDING_REGEXP.exec(serviceBinding.toString())?.groups || {}; if (!binding || !service) { @@ -1006,6 +1008,7 @@ function getBindingsFromArgs(args: PagesDevArguments): Partial< binding, service: serviceName, environment, + entrypoint, }; }) .filter(Boolean) as NonNullable; diff --git a/packages/wrangler/src/versions/upload.ts b/packages/wrangler/src/versions/upload.ts index 32d843567f6f..868ff4036bec 100644 --- a/packages/wrangler/src/versions/upload.ts +++ b/packages/wrangler/src/versions/upload.ts @@ -286,11 +286,6 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m assets: config.assets, // enable the cache when publishing bypassAssetCache: false, - services: config.services, - // We don't set workerDefinitions here, - // because we don't want to apply the dev-time - // facades on top of it - workerDefinitions: undefined, // We want to know if the build is for development or publishing // This could potentially cause issues as we no longer have identical behaviour between dev and deploy? targetConsumer: "deploy", diff --git a/packages/wrangler/templates/facade.d.ts b/packages/wrangler/templates/facade.d.ts index bfd2f4c6c3e2..d52380632f86 100644 --- a/packages/wrangler/templates/facade.d.ts +++ b/packages/wrangler/templates/facade.d.ts @@ -1,9 +1,11 @@ declare module "__ENTRY_POINT__" { - import type { Middleware } from "./middleware/common"; - const worker: ExportedHandler & { - middleware?: Middleware[]; - envWrappers: ((env: Record) => Record)[]; - }; + import { Middleware } from "./middleware/common"; + import { WorkerEntrypoint } from "cloudflare:workers"; + + export type WorkerEntrypointConstructor = typeof WorkerEntrypoint; + export type WithMiddleware = T & { middleware?: Middleware[] }; + + const worker: WithMiddleware; export default worker; } diff --git a/packages/wrangler/templates/middleware/loader-modules.ts b/packages/wrangler/templates/middleware/loader-modules.ts index 42c6492431ad..4526de9146aa 100644 --- a/packages/wrangler/templates/middleware/loader-modules.ts +++ b/packages/wrangler/templates/middleware/loader-modules.ts @@ -1,22 +1,20 @@ -// // This loads all middlewares exposed on the middleware object -// // and then starts the invocation chain. -// // The big idea is that we can add these to the middleware export dynamically -// // through wrangler, or we can potentially let users directly add them as a sort -// // of "plugin" system. - -import worker from "__ENTRY_POINT__"; -import { - __facade_invoke__, - __facade_register__, - Dispatcher, - Middleware, -} from "./common"; - -// We need to preserve all of the exports from the worker +// This loads all middlewares exposed on the middleware object and then starts +// the invocation chain. The big idea is that we can add these to the middleware +// export dynamically through wrangler, or we can potentially let users directly +// add them as a sort of "plugin" system. + +import ENTRY from "__ENTRY_POINT__"; +import { __facade_invoke__, __facade_register__, Dispatcher } from "./common"; +import type { + WithMiddleware, + WorkerEntrypointConstructor, +} from "__ENTRY_POINT__"; + +// Preserve all the exports from the worker export * from "__ENTRY_POINT__"; class __Facade_ScheduledController__ implements ScheduledController { - #noRetry: ScheduledController["noRetry"]; + readonly #noRetry: ScheduledController["noRetry"]; constructor( readonly scheduledTime: number, @@ -35,71 +33,33 @@ class __Facade_ScheduledController__ implements ScheduledController { } } -const __facade_modules_fetch__: ExportedHandlerFetchHandler = function ( - request, - env, - ctx -) { - if (worker.fetch === undefined) - throw new Error("Handler does not export a fetch() function."); - return worker.fetch(request, env, ctx); -}; - -function getMaskedEnv(rawEnv: unknown) { - let env = rawEnv as Record; - if (worker.envWrappers && worker.envWrappers.length > 0) { - for (const wrapFn of worker.envWrappers) { - env = wrapFn(env); - } +function wrapExportedHandler( + worker: WithMiddleware +): ExportedHandler { + // If we don't have any middleware defined, just return the handler as is + if (worker.middleware === undefined || worker.middleware.length === 0) { + return worker; + } + // Otherwise, register all middleware once + for (const middleware of worker.middleware) { + __facade_register__(middleware); } - return env; -} - -/** - * This type is here to cause a type error if a new export handler is added to - * `ExportHandler` without it being included in the `facade` below. - */ -type MissingExportHandlers = Omit< - Required, - "tail" | "trace" | "scheduled" | "queue" | "test" | "email" | "fetch" ->; - -let registeredMiddleware = false; - -const facade: ExportedHandler & MissingExportHandlers = { - ...(worker.tail && { - tail: maskHandlerEnv(worker.tail), - }), - ...(worker.trace && { - trace: maskHandlerEnv(worker.trace), - }), - ...(worker.scheduled && { - scheduled: maskHandlerEnv(worker.scheduled), - }), - ...(worker.queue && { - queue: maskHandlerEnv(worker.queue), - }), - ...(worker.test && { - test: maskHandlerEnv(worker.test), - }), - ...(worker.email && { - email: maskHandlerEnv(worker.email), - }), - fetch(request, rawEnv, ctx) { - const env = getMaskedEnv(rawEnv); - // Get the chain of middleware from the worker object - if (worker.middleware && worker.middleware.length > 0) { - // Make sure we only register middleware once: - // https://github.com/cloudflare/workers-sdk/issues/2386#issuecomment-1614715911 - if (!registeredMiddleware) { - registeredMiddleware = true; - for (const middleware of worker.middleware) { - __facade_register__(middleware); - } - } + const fetchDispatcher: ExportedHandlerFetchHandler = function ( + request, + env, + ctx + ) { + if (worker.fetch === undefined) { + throw new Error("Handler does not export a fetch() function."); + } + return worker.fetch(request, env, ctx); + }; - const __facade_modules_dispatch__: Dispatcher = function (type, init) { + return { + ...worker, + fetch(request, env, ctx) { + const dispatcher: Dispatcher = function (type, init) { if (type === "scheduled" && worker.scheduled !== undefined) { const controller = new __Facade_ScheduledController__( Date.now(), @@ -109,28 +69,65 @@ const facade: ExportedHandler & MissingExportHandlers = { return worker.scheduled(controller, env, ctx); } }; + return __facade_invoke__(request, env, ctx, dispatcher, fetchDispatcher); + }, + }; +} + +function wrapWorkerEntrypoint( + klass: WithMiddleware +): WorkerEntrypointConstructor { + // If we don't have any middleware defined, just return the handler as is + if (klass.middleware === undefined || klass.middleware.length === 0) { + return klass; + } + // Otherwise, register all middleware once + for (const middleware of klass.middleware) { + __facade_register__(middleware); + } + // `extend`ing `klass` here so other RPC methods remain callable + return class extends klass { + #fetchDispatcher: ExportedHandlerFetchHandler> = ( + request, + env, + ctx + ) => { + this.env = env; + this.ctx = ctx; + if (super.fetch === undefined) { + throw new Error("Entrypoint class does not define a fetch() function."); + } + return super.fetch(request); + }; + + #dispatcher: Dispatcher = (type, init) => { + if (type === "scheduled" && super.scheduled !== undefined) { + const controller = new __Facade_ScheduledController__( + Date.now(), + init.cron ?? "", + () => {} + ); + return super.scheduled(controller); + } + }; + + fetch(request: Request) { return __facade_invoke__( request, - env, - ctx, - __facade_modules_dispatch__, - __facade_modules_fetch__ + this.env, + this.ctx, + this.#dispatcher, + this.#fetchDispatcher ); - } else { - // We didn't have any middleware so we can skip the invocation chain, - // and just call the fetch handler directly - - // We "don't care" if this is undefined as we want to have the same behavior - // as if the worker completely bypassed middleware. - return __facade_modules_fetch__(request, env, ctx); } - }, -}; - -type HandlerFn = (data: D, env: unknown, ctx: ExecutionContext) => R; -function maskHandlerEnv(handler: HandlerFn): HandlerFn { - return (data, env, ctx) => handler(data, getMaskedEnv(env), ctx); + }; } -export default facade; +let WRAPPED_ENTRY: ExportedHandler | WorkerEntrypointConstructor | undefined; +if (typeof ENTRY === "object") { + WRAPPED_ENTRY = wrapExportedHandler(ENTRY); +} else if (typeof ENTRY === "function") { + WRAPPED_ENTRY = wrapWorkerEntrypoint(ENTRY); +} +export default WRAPPED_ENTRY; diff --git a/packages/wrangler/templates/middleware/middleware-d1-beta.d.ts b/packages/wrangler/templates/middleware/middleware-d1-beta.d.ts deleted file mode 100644 index 5ee0e6f6476a..000000000000 --- a/packages/wrangler/templates/middleware/middleware-d1-beta.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare module "config:middleware/d1-beta" { - export const D1_IMPORTS: string[]; -} diff --git a/packages/wrangler/templates/middleware/middleware-ensure-req-body-drained.ts b/packages/wrangler/templates/middleware/middleware-ensure-req-body-drained.ts index dfc8fd1e0ef4..c6b4e7362e85 100644 --- a/packages/wrangler/templates/middleware/middleware-ensure-req-body-drained.ts +++ b/packages/wrangler/templates/middleware/middleware-ensure-req-body-drained.ts @@ -16,4 +16,3 @@ const drainBody: Middleware = async (request, env, _ctx, middlewareCtx) => { }; export default drainBody; -export const wrap = undefined; diff --git a/packages/wrangler/templates/middleware/middleware-miniflare3-json-error.ts b/packages/wrangler/templates/middleware/middleware-miniflare3-json-error.ts index 937778107960..b1850f814f90 100644 --- a/packages/wrangler/templates/middleware/middleware-miniflare3-json-error.ts +++ b/packages/wrangler/templates/middleware/middleware-miniflare3-json-error.ts @@ -30,4 +30,3 @@ const jsonError: Middleware = async (request, env, _ctx, middlewareCtx) => { }; export default jsonError; -export const wrap = undefined; diff --git a/packages/wrangler/templates/middleware/middleware-multiworker-dev.d.ts b/packages/wrangler/templates/middleware/middleware-multiworker-dev.d.ts deleted file mode 100644 index 704b273c3b4f..000000000000 --- a/packages/wrangler/templates/middleware/middleware-multiworker-dev.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "config:middleware/multiworker-dev" { - import type { WorkerRegistry } from "../../src/dev-registry"; - export const workers: WorkerRegistry; -} diff --git a/packages/wrangler/templates/middleware/middleware-multiworker-dev.ts b/packages/wrangler/templates/middleware/middleware-multiworker-dev.ts deleted file mode 100644 index 5cb6b7a4dc58..000000000000 --- a/packages/wrangler/templates/middleware/middleware-multiworker-dev.ts +++ /dev/null @@ -1,68 +0,0 @@ -// @ts-nocheck -/// - -import { workers } from "config:middleware/multiworker-dev"; -import type { WorkerRegistry } from "../../src/dev-registry"; - -class Fetcher { - #name: string; - #details: WorkerRegistry[string]; - constructor(name: string, details: WorkerRegistry[string]) { - this.#name = name; - this.#details = details; - } - - async fetch(...reqArgs: Parameters) { - const reqFromArgs = new Request(...reqArgs); - if (this.#details.headers) { - for (const [key, value] of Object.entries(this.#details.headers)) { - // In remote mode, you need to add a couple of headers - // to make sure it's talking to the 'dev' preview session - // (much like wrangler dev already does via proxy.ts) - reqFromArgs.headers.set(key, value); - } - return (env[this.#name] as Fetcher).fetch(reqFromArgs); - } - - const url = new URL(reqFromArgs.url); - if (this.#details.protocol !== undefined) { - url.protocol = this.#details.protocol; - } - if (this.#details.host !== undefined) { - url.host = this.#details.host; - } - if (this.#details.port !== undefined) { - url.port = this.#details.port.toString(); - } - - const request = new Request(url.toString(), reqFromArgs); - return fetch(request); - } -} - -export function wrap(env: Record) { - const facadeEnv = { ...env }; - // For every Worker definition that's available, - // create a fetcher for it on the facade env. - // for const [name, binding] of env - // if Workers[name] - // const details = Workers[name]; - - for (const [name, details] of Object.entries(workers as WorkerRegistry)) { - if (details) { - facadeEnv[name] = new Fetcher(name, details); - } else { - // This means there's no dev binding available. - // Let's use whatever's available, or put a shim with a message. - facadeEnv[name] = facadeEnv[name] || { - async fetch() { - return new Response( - `You should start up wrangler dev --local on the ${name} worker`, - { status: 404 } - ); - }, - }; - } - } - return facadeEnv; -} diff --git a/packages/wrangler/turbo.json b/packages/wrangler/turbo.json index 9c5d50f3a4bb..84a5e61e65a8 100644 --- a/packages/wrangler/turbo.json +++ b/packages/wrangler/turbo.json @@ -40,7 +40,8 @@ "CI_OS", "SENTRY_DSN", "SYSTEMROOT", - "WRANGLER_DISABLE_REQUEST_BODY_DRAINING" + "WRANGLER_DISABLE_REQUEST_BODY_DRAINING", + "WRANGLER_WORKER_REGISTRY_PORT" ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dba51a8854a0..5cb0813afe18 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,8 +78,8 @@ importers: specifier: ^2.27.1 version: 2.27.1 '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 '@turbo/gen': specifier: ^1.10.13 version: 1.10.14(@types/node@20.1.7)(typescript@4.9.5) @@ -102,8 +102,8 @@ importers: specifier: workspace:* version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 undici: specifier: ^5.28.3 version: 5.28.3 @@ -168,14 +168,29 @@ importers: specifier: workspace:* version: link:../../packages/wrangler + fixtures/entrypoints-rpc-tests: + devDependencies: + '@cloudflare/workers-tsconfig': + specifier: workspace:* + version: link:../../packages/workers-tsconfig + ts-dedent: + specifier: ^2.2.0 + version: 2.2.0 + undici: + specifier: ^5.28.3 + version: 5.28.3 + wrangler: + specifier: workspace:* + version: link:../../packages/wrangler + fixtures/external-durable-objects-app: devDependencies: '@cloudflare/workers-tsconfig': specifier: workspace:* version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 undici: specifier: ^5.28.3 version: 5.28.3 @@ -189,8 +204,8 @@ importers: specifier: workspace:* version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 concurrently: specifier: ^8.2.1 version: 8.2.1 @@ -207,8 +222,8 @@ importers: specifier: workspace:* version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 undici: specifier: ^5.28.3 version: 5.28.3 @@ -222,8 +237,8 @@ importers: specifier: workspace:* version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 undici: specifier: ^5.28.3 version: 5.28.3 @@ -272,8 +287,8 @@ importers: specifier: workspace:* version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 '@types/node': specifier: ^17.0.33 version: 17.0.45 @@ -303,8 +318,8 @@ importers: specifier: workspace:* version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 undici: specifier: ^5.28.3 version: 5.28.3 @@ -330,8 +345,8 @@ importers: specifier: workspace:* version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 undici: specifier: ^5.28.3 version: 5.28.3 @@ -345,8 +360,8 @@ importers: specifier: workspace:* version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 pages-plugin-example: specifier: workspace:* version: link:../pages-plugin-example @@ -387,8 +402,8 @@ importers: specifier: workspace:* version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 undici: specifier: ^5.28.3 version: 5.28.3 @@ -408,8 +423,8 @@ importers: specifier: workspace:* version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 pages-plugin-example: specifier: workspace:* version: link:../pages-plugin-example @@ -441,8 +456,8 @@ importers: specifier: workspace:* version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 undici: specifier: ^5.28.3 version: 5.28.3 @@ -695,8 +710,8 @@ importers: fixtures/worker-ts: devDependencies: '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 wrangler: specifier: workspace:* version: link:../../packages/wrangler @@ -762,8 +777,8 @@ importers: specifier: workspace:* version: link:../workers-tsconfig '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 '@iarna/toml': specifier: ^3.0.0 version: 3.0.0 @@ -885,8 +900,8 @@ importers: specifier: '*' version: link:../eslint-config-worker '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 '@types/cookie': specifier: ^0.6.0 version: 0.6.0 @@ -916,7 +931,7 @@ importers: version: 8.49.0 eslint-config-turbo: specifier: latest - version: 1.12.4(eslint@8.49.0) + version: 1.11.3(eslint@8.49.0) eslint-plugin-import: specifier: 2.26.x version: 2.26.0(@typescript-eslint/parser@6.7.2)(eslint@8.49.0) @@ -939,8 +954,8 @@ importers: specifier: '*' version: link:../eslint-config-worker '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 mustache: specifier: ^4.2.0 version: 4.2.0 @@ -973,8 +988,8 @@ importers: specifier: ^4.1.0 version: 4.1.0 '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 '@types/mime': specifier: ^3.0.4 version: 3.0.4 @@ -1140,8 +1155,8 @@ importers: specifier: workspace:* version: link:../workers-tsconfig '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 '@types/service-worker-mock': specifier: ^2.0.1 version: 2.0.1 @@ -1168,8 +1183,8 @@ importers: specifier: workspace:* version: link:../eslint-config-worker '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 '@types/cookie': specifier: ^0.5.1 version: 0.5.1 @@ -1205,8 +1220,8 @@ importers: specifier: workspace:* version: link:../workers-tsconfig '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 typescript: specifier: ^4.5.5 version: 4.9.5 @@ -1239,8 +1254,8 @@ importers: specifier: workspace:^ version: link:../workers-tsconfig '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 esbuild: specifier: 0.16.3 version: 0.16.3 @@ -1492,8 +1507,8 @@ importers: specifier: workspace:* version: link:../workers-tsconfig '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 wrangler: specifier: workspace:* version: link:../wrangler @@ -1566,8 +1581,8 @@ importers: specifier: workspace:* version: link:../workers-tsconfig '@cloudflare/workers-types': - specifier: ^4.20240320.1 - version: 4.20240320.1 + specifier: ^4.20240402.0 + version: 4.20240402.0 '@cspotcode/source-map-support': specifier: 0.8.1 version: 0.8.1 @@ -1676,6 +1691,9 @@ importers: dotenv: specifier: ^16.0.0 version: 16.0.0 + es-module-lexer: + specifier: ^1.3.0 + version: 1.3.1 esbuild-jest: specifier: 0.5.0 version: 0.5.0(esbuild@0.17.19)(supports-color@9.2.2) @@ -4193,14 +4211,14 @@ packages: /@cloudflare/workers-types@3.19.0: resolution: {integrity: sha512-0FRcsz7Ea3jT+gc5gKPIYciykm1bbAaTpygdzpCwGt0RL+V83zWnYN30NWDW4rIHj/FHtz+MIuBKS61C8l7AzQ==} - /@cloudflare/workers-types@4.20240320.1: - resolution: {integrity: sha512-CiYtVpQURPgQqtBKkmOAnfPElVZuD7Xyf1IxKtKp2B4aB9gnooapwJhzeY8c4Ls4u17SgMS0MprOkrgYwzZ6xg==} - dev: true - /@cloudflare/workers-types@4.20240329.0: resolution: {integrity: sha512-AbzgvSQjG8Nci4xxQEcjTTVjiWXgOQnFIbIHtEZXteHiMGDXMWGegjWBo5JHGsZCq+U5V/SD5EnlypQnUQEoig==} dev: true + /@cloudflare/workers-types@4.20240402.0: + resolution: {integrity: sha512-GEtg71Gs5iBIQBQB0sBkEOJrmJGpenT2XAODwbvfWADwQpDxgEQfGrcOhaJfLPCfvNpKaMxcBlH9cekWwQEsLQ==} + dev: true + /@cnakazawa/watch@1.0.4: resolution: {integrity: sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==} engines: {node: '>=0.1.95'} @@ -11179,7 +11197,6 @@ packages: /es-module-lexer@1.3.1: resolution: {integrity: sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==} - dev: false /es-set-tostringtag@2.0.1: resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} @@ -11490,13 +11507,13 @@ packages: eslint: 8.49.0 dev: true - /eslint-config-turbo@1.12.4(eslint@8.49.0): - resolution: {integrity: sha512-5hqEaV6PNmAYLL4RTmq74OcCt8pgzOLnfDVPG/7PUXpQ0Mpz0gr926oCSFukywKKXjdum3VHD84S7Z9A/DqTAw==} + /eslint-config-turbo@1.11.3(eslint@8.49.0): + resolution: {integrity: sha512-v7CHpAHodBKlj+r+R3B2DJlZbCjpZLnK7gO/vCRk/Lc+tlD/f04wM6rmHlerevOlchtmwARilRLBnmzNLffTyQ==} peerDependencies: eslint: '>6.6.0' dependencies: eslint: 8.49.0 - eslint-plugin-turbo: 1.12.4(eslint@8.49.0) + eslint-plugin-turbo: 1.11.3(eslint@8.49.0) dev: false /eslint-import-resolver-node@0.3.7: @@ -11923,8 +11940,8 @@ packages: - typescript dev: true - /eslint-plugin-turbo@1.12.4(eslint@8.49.0): - resolution: {integrity: sha512-3AGmXvH7E4i/XTWqBrcgu+G7YKZJV/8FrEn79kTd50ilNsv+U3nS2IlcCrQB6Xm2m9avGD9cadLzKDR1/UF2+g==} + /eslint-plugin-turbo@1.11.3(eslint@8.49.0): + resolution: {integrity: sha512-R5ftTTWQzEYaKzF5g6m/MInCU8pIN+2TLL+S50AYBr1enwUovdZmnZ1HDwFMaxIjJ8x5ah+jvAzql5IJE9VWaA==} peerDependencies: eslint: '>6.6.0' dependencies: