From 54979b3fa116a320a720caa23f4f377c3aecf519 Mon Sep 17 00:00:00 2001 From: emily-shen <69125074+emily-shen@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:48:15 +0100 Subject: [PATCH 1/6] straightforward removals of deprecated APIs for miniflare v5 (#14585) --- .changeset/drop-miniflare-v2-migration.md | 7 ++ .changeset/remove-maxretires-typo.md | 7 ++ .changeset/remove-miniflare-cli-binary.md | 7 ++ .changeset/remove-supported-compat-date.md | 7 ++ packages/miniflare/AGENTS.md | 3 +- packages/miniflare/bootstrap.js | 11 ---- packages/miniflare/package.json | 6 +- packages/miniflare/src/plugins/d1/index.ts | 9 --- packages/miniflare/src/plugins/kv/index.ts | 14 ---- packages/miniflare/src/plugins/r2/index.ts | 9 --- .../miniflare/src/plugins/shared/index.ts | 61 +----------------- packages/miniflare/src/runtime/index.ts | 22 +------ .../miniflare/src/workers/queues/schemas.ts | 27 +++----- .../migrations/3.20230821.0/README.md | 42 ------------ .../3.20230821.0/d1/DATABASE/db.sqlite | Bin 12288 -> 0 bytes ...5c1a39c5baea36f40e912e81e6000637434f5e7544 | 1 - .../3.20230821.0/kv/NAMESPACE/db.sqlite | Bin 16384 -> 0 bytes ...2966feb09ac09ec3cc4b27dcff000637435043236c | 1 - .../3.20230821.0/r2/BUCKET/db.sqlite | Bin 28672 -> 0 bytes .../miniflare/test/plugins/d1/index.spec.ts | 26 -------- .../miniflare/test/plugins/kv/index.spec.ts | 22 ------- .../miniflare/test/plugins/r2/index.spec.ts | 23 ------- packages/miniflare/turbo.json | 2 +- tools/deployments/validate-changesets.ts | 7 +- 24 files changed, 48 insertions(+), 266 deletions(-) create mode 100644 .changeset/drop-miniflare-v2-migration.md create mode 100644 .changeset/remove-maxretires-typo.md create mode 100644 .changeset/remove-miniflare-cli-binary.md create mode 100644 .changeset/remove-supported-compat-date.md delete mode 100755 packages/miniflare/bootstrap.js delete mode 100644 packages/miniflare/test/fixtures/migrations/3.20230821.0/README.md delete mode 100644 packages/miniflare/test/fixtures/migrations/3.20230821.0/d1/DATABASE/db.sqlite delete mode 100644 packages/miniflare/test/fixtures/migrations/3.20230821.0/kv/NAMESPACE/blobs/c708857cd996a02e1c61a4d5af45519baf21ce5c1a39c5baea36f40e912e81e6000637434f5e7544 delete mode 100644 packages/miniflare/test/fixtures/migrations/3.20230821.0/kv/NAMESPACE/db.sqlite delete mode 100644 packages/miniflare/test/fixtures/migrations/3.20230821.0/r2/BUCKET/blobs/776759739fb970e463921f00822d78ae8455152966feb09ac09ec3cc4b27dcff000637435043236c delete mode 100644 packages/miniflare/test/fixtures/migrations/3.20230821.0/r2/BUCKET/db.sqlite diff --git a/.changeset/drop-miniflare-v2-migration.md b/.changeset/drop-miniflare-v2-migration.md new file mode 100644 index 00000000000..132d8664158 --- /dev/null +++ b/.changeset/drop-miniflare-v2-migration.md @@ -0,0 +1,7 @@ +--- +"miniflare": major +--- + +Drop miniflare v2 storage migration + +The `migrateDatabase()` helper that migrated KV, R2, and D1 SQLite databases from the miniflare v2/early-v3 storage layout to the current Durable Object-based layout has been removed. This migration path was introduced in 2023 and is no longer needed. diff --git a/.changeset/remove-maxretires-typo.md b/.changeset/remove-maxretires-typo.md new file mode 100644 index 00000000000..0700e64113e --- /dev/null +++ b/.changeset/remove-maxretires-typo.md @@ -0,0 +1,7 @@ +--- +"miniflare": major +--- + +Remove deprecated `maxRetires` typo alias in Queue consumer options + +The `maxRetires` option (a typo of `maxRetries`) has been removed from `QueueConsumerOptionsSchema`. Use `maxRetries` instead. diff --git a/.changeset/remove-miniflare-cli-binary.md b/.changeset/remove-miniflare-cli-binary.md new file mode 100644 index 00000000000..edf2f9f85a3 --- /dev/null +++ b/.changeset/remove-miniflare-cli-binary.md @@ -0,0 +1,7 @@ +--- +"miniflare": major +--- + +Remove dummy CLI binary + +The `miniflare` bin entry and `bootstrap.js` stub that printed a "use `npx wrangler dev`" error have been removed. This stub has been present since miniflare v3 and is no longer needed. diff --git a/.changeset/remove-supported-compat-date.md b/.changeset/remove-supported-compat-date.md new file mode 100644 index 00000000000..3016cd33818 --- /dev/null +++ b/.changeset/remove-supported-compat-date.md @@ -0,0 +1,7 @@ +--- +"miniflare": major +--- + +Remove deprecated `supportedCompatibilityDate` export + +The `supportedCompatibilityDate` export has been removed. Use `new Date().toISOString().slice(0, 10)` instead. diff --git a/packages/miniflare/AGENTS.md b/packages/miniflare/AGENTS.md index 0491dbf6709..83eeca59ba3 100644 --- a/packages/miniflare/AGENTS.md +++ b/packages/miniflare/AGENTS.md @@ -16,14 +16,13 @@ Local dev simulator for Cloudflare Workers, powered by workerd runtime. Main cla - `src/runtime/config/generated/workerd.ts` — Generated workerd Cap'n Proto config types - `test/` — Tests (`.spec.ts` naming, NOT `.test.ts`) - `test/test-shared/` — Shared test utilities -- `bootstrap.js` — Deprecated CLI stub (prints error directing to `wrangler dev`) - `scripts/build.mjs` — Custom esbuild build script ## Build - Custom `scripts/build.mjs` bundles `src/index.ts` as CJS → `dist/src/index.js` - Also compiles ~30 embedded worker scripts via `worker:...` virtual import scheme — each triggers a nested esbuild sub-build producing standalone ESM worker scripts -- Build outputs: `dist/`, `bootstrap.js`, `worker-metafiles/` +- Build outputs: `dist/`, `worker-metafiles/` ## Lint Status (Transitional) diff --git a/packages/miniflare/bootstrap.js b/packages/miniflare/bootstrap.js deleted file mode 100755 index a5e88263494..00000000000 --- a/packages/miniflare/bootstrap.js +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env node -const { Log } = require("."); - -const log = new Log(); -log.error( - [ - "`miniflare@3` no longer includes a CLI. Please use `npx wrangler dev` instead.", - "As of `wrangler@3`, this will use Miniflare by default.", - "See https://miniflare.dev/get-started/migrating for more details.", - ].join("\n") -); diff --git a/packages/miniflare/package.json b/packages/miniflare/package.json index 2400e8f5414..e9968ff0b33 100644 --- a/packages/miniflare/package.json +++ b/packages/miniflare/package.json @@ -20,13 +20,9 @@ "url": "https://github.com/cloudflare/workers-sdk.git", "directory": "packages/miniflare" }, - "bin": { - "miniflare": "bootstrap.js" - }, "files": [ "dist/src", - "dist/local-explorer-ui", - "bootstrap.js" + "dist/local-explorer-ui" ], "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", diff --git a/packages/miniflare/src/plugins/d1/index.ts b/packages/miniflare/src/plugins/d1/index.ts index 007f73d68eb..cff17189621 100644 --- a/packages/miniflare/src/plugins/d1/index.ts +++ b/packages/miniflare/src/plugins/d1/index.ts @@ -8,7 +8,6 @@ import { getMiniflareObjectBindings, getPersistPath, getUserBindingServiceName, - migrateDatabase, namespaceEntries, namespaceKeys, objectEntryWorker, @@ -117,7 +116,6 @@ export const D1_PLUGIN: Plugin< sharedOptions, tmpPath, defaultPersistRoot, - log, unsafeStickyBlobs, }) { const persist = sharedOptions.d1Persist; @@ -191,13 +189,6 @@ export const D1_PLUGIN: Plugin< }, }; services.push(storageService, objectService); - - for (const [, database] of databases) { - if (database.remoteProxyConnectionString) { - continue; - } - await migrateDatabase(log, uniqueKey, persistPath, database.id); - } } return services; diff --git a/packages/miniflare/src/plugins/kv/index.ts b/packages/miniflare/src/plugins/kv/index.ts index ff79ca4100c..4815fac1be5 100644 --- a/packages/miniflare/src/plugins/kv/index.ts +++ b/packages/miniflare/src/plugins/kv/index.ts @@ -7,7 +7,6 @@ import { buildRemoteProxyProps, getMiniflareObjectBindings, getPersistPath, - migrateDatabase, namespaceEntries, namespaceKeys, objectEntryWorker, @@ -140,7 +139,6 @@ export const KV_PLUGIN: Plugin< sharedOptions, tmpPath, defaultPersistRoot, - log, unsafeStickyBlobs, }) { const persist = sharedOptions.kvPersist; @@ -214,18 +212,6 @@ export const KV_PLUGIN: Plugin< }, }; services.push(storageService, objectService); - - // Before the switch to Durable Object simulators, Miniflare stored - // databases alongside blobs in a namespace specific directory. To avoid - // another breaking change to the persistence location, migrate SQLite - // databases from the old location to the new location. Blobs are still - // stored in the same location. - for (const [, namespace] of namespaces) { - if (namespace.remoteProxyConnectionString) { - continue; - } - await migrateDatabase(log, uniqueKey, persistPath, namespace.id); - } } if (isWorkersSitesEnabled(options)) { diff --git a/packages/miniflare/src/plugins/r2/index.ts b/packages/miniflare/src/plugins/r2/index.ts index 59543f5d409..db01c782679 100644 --- a/packages/miniflare/src/plugins/r2/index.ts +++ b/packages/miniflare/src/plugins/r2/index.ts @@ -8,7 +8,6 @@ import { getMiniflareObjectBindings, getPersistPath, getUserBindingServiceName, - migrateDatabase, namespaceEntries, namespaceKeys, objectEntryWorker, @@ -127,7 +126,6 @@ export const R2_PLUGIN: Plugin< sharedOptions, tmpPath, defaultPersistRoot, - log, unsafeStickyBlobs, }) { const persist = sharedOptions.r2Persist; @@ -200,13 +198,6 @@ export const R2_PLUGIN: Plugin< }, }; services.push(storageService, objectService); - - for (const [, bucket] of buckets) { - if (bucket.remoteProxyConnectionString) { - continue; - } - await migrateDatabase(log, uniqueKey, persistPath, bucket.id); - } } return services; diff --git a/packages/miniflare/src/plugins/shared/index.ts b/packages/miniflare/src/plugins/shared/index.ts index baf5928461c..13b9ffc1d58 100644 --- a/packages/miniflare/src/plugins/shared/index.ts +++ b/packages/miniflare/src/plugins/shared/index.ts @@ -1,11 +1,8 @@ -import crypto, { createHash } from "node:crypto"; -import { existsSync } from "node:fs"; -import fs from "node:fs/promises"; +import { createHash } from "node:crypto"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { z } from "zod"; import { MiniflareCoreError, PathSchema } from "../../shared"; -import { sanitisePath } from "../../workers"; import type { Extension, Service, @@ -284,62 +281,6 @@ export function getPersistPath( return result.replaceAll("\\", "/"); } -// https://github.com/cloudflare/workerd/blob/81d97010e44f848bb95d0083e2677bca8d1658b7/src/workerd/server/workerd-api.c%2B%2B#L436 -function durableObjectNamespaceIdFromName(uniqueKey: string, name: string) { - const key = crypto.createHash("sha256").update(uniqueKey).digest(); - const nameHmac = crypto - .createHmac("sha256", key) - .update(name) - .digest() - .subarray(0, 16); - const hmac = crypto - .createHmac("sha256", key) - .update(nameHmac) - .digest() - .subarray(0, 16); - return Buffer.concat([nameHmac, hmac]).toString("hex"); -} - -export async function migrateDatabase( - log: Log, - uniqueKey: string, - persistPath: string, - namespace: string -) { - // Check if database exists at previous location - const sanitisedNamespace = sanitisePath(namespace); - const previousDir = path.join(persistPath, sanitisedNamespace); - const previousPath = path.join(previousDir, "db.sqlite"); - const previousWalPath = path.join(previousDir, "db.sqlite-wal"); - if (!existsSync(previousPath)) return; - - // Move database to new location, if database isn't already there - const id = durableObjectNamespaceIdFromName(uniqueKey, namespace); - const newDir = path.join(persistPath, uniqueKey); - const newPath = path.join(newDir, `${id}.sqlite`); - const newWalPath = path.join(newDir, `${id}.sqlite-wal`); - if (existsSync(newPath)) { - log.debug( - `Not migrating ${previousPath} to ${newPath} as it already exists` - ); - return; - } - - log.debug(`Migrating ${previousPath} to ${newPath}...`); - await fs.mkdir(newDir, { recursive: true }); - - try { - await fs.copyFile(previousPath, newPath); - if (existsSync(previousWalPath)) { - await fs.copyFile(previousWalPath, newWalPath); - } - await fs.unlink(previousPath); - await fs.unlink(previousWalPath); - } catch (e) { - log.warn(`Error migrating ${previousPath} to ${newPath}: ${e}`); - } -} - /** * Service names for remote bindings should be unique depending on the remote proxy connection * string (since in theory different remote bindings can have different remote proxy connections), diff --git a/packages/miniflare/src/runtime/index.ts b/packages/miniflare/src/runtime/index.ts index 7bd790cd3ec..35a9faa5296 100644 --- a/packages/miniflare/src/runtime/index.ts +++ b/packages/miniflare/src/runtime/index.ts @@ -6,9 +6,7 @@ import path from "node:path"; import rl from "node:readline"; import { Readable, Transform } from "node:stream"; import { $ as $colors, red } from "kleur/colors"; -import workerdPath, { - compatibilityDate as workerdCompatibilityDate, -} from "workerd"; +import workerdPath from "workerd"; import { z } from "zod"; import { SERVICE_LOOPBACK, SOCKET_ENTRY } from "../plugins"; import { MiniflareCoreError } from "../shared"; @@ -385,21 +383,3 @@ export class Runtime { } export * from "./config"; - -/** - * Gets a safe compatibility date from workerd. If the workerd compatibility - * date is in the future, returns today's date instead. This handles the case - * where workerd releases set their compatibility date up to 7 days in the future. - */ -function getSafeCompatibilityDate(): string { - const today = new Date().toISOString().slice(0, 10); - if (workerdCompatibilityDate > today) { - return today; - } - return workerdCompatibilityDate; -} - -/** - * @deprecated Use today's date as the compatibility date instead: `new Date().toISOString().slice(0, 10)` - */ -export const supportedCompatibilityDate = getSafeCompatibilityDate(); diff --git a/packages/miniflare/src/workers/queues/schemas.ts b/packages/miniflare/src/workers/queues/schemas.ts index d714a273ce1..985c359a72a 100644 --- a/packages/miniflare/src/workers/queues/schemas.ts +++ b/packages/miniflare/src/workers/queues/schemas.ts @@ -21,24 +21,15 @@ export type QueueProducer = z.infer; export const QueueProducersSchema = /* @__PURE__ */ z.record(QueueProducerSchema); -export const QueueConsumerOptionsSchema = /* @__PURE__ */ z - .object({ - // https://developers.cloudflare.com/queues/platform/configuration/#consumer - // https://developers.cloudflare.com/queues/platform/limits/ - maxBatchSize: z.number().min(0).max(100).optional(), - maxBatchTimeout: z.number().min(0).max(60).optional(), // seconds - maxRetires: z.number().min(0).max(100).optional(), // deprecated - maxRetries: z.number().min(0).max(100).optional(), - deadLetterQueue: z.ostring(), - retryDelay: QueueMessageDelaySchema, - }) - .transform((queue) => { - if (queue.maxRetires !== undefined) { - queue.maxRetries = queue.maxRetires; - } - - return queue as Omit; - }); +export const QueueConsumerOptionsSchema = /* @__PURE__ */ z.object({ + // https://developers.cloudflare.com/queues/platform/configuration/#consumer + // https://developers.cloudflare.com/queues/platform/limits/ + maxBatchSize: z.number().min(0).max(100).optional(), + maxBatchTimeout: z.number().min(0).max(60).optional(), // seconds + maxRetries: z.number().min(0).max(100).optional(), + deadLetterQueue: z.ostring(), + retryDelay: QueueMessageDelaySchema, +}); export const QueueConsumerSchema = /* @__PURE__ */ z.intersection( QueueConsumerOptionsSchema, z.object({ workerName: z.string() }) diff --git a/packages/miniflare/test/fixtures/migrations/3.20230821.0/README.md b/packages/miniflare/test/fixtures/migrations/3.20230821.0/README.md deleted file mode 100644 index b7122c4a4a1..00000000000 --- a/packages/miniflare/test/fixtures/migrations/3.20230821.0/README.md +++ /dev/null @@ -1,42 +0,0 @@ -The contents of this directory were generated with the following script using -`miniflare@3.20230821.0`: - -```js -import path from "node:path"; -import url from "node:url"; -import { Miniflare } from "miniflare"; - -const __filename = url.fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const mf = new Miniflare({ - script: "", - modules: true, - - kvPersist: path.join(__dirname, "kv"), - kvNamespaces: ["NAMESPACE"], - - r2Persist: path.join(__dirname, "r2"), - r2Buckets: ["BUCKET"], - - d1Persist: path.join(__dirname, "d1"), - d1Databases: ["DATABASE"], -}); - -const kvNamespace = await mf.getKVNamespace("NAMESPACE"); -await kvNamespace.put("key", "value"); - -const r2Bucket = await mf.getR2Bucket("BUCKET"); -await r2Bucket.put("key", "value"); - -const d1Database = await mf.getD1Database("DATABASE"); -await d1Database.exec( - "CREATE TABLE entries (key TEXT PRIMARY KEY, value TEXT);" -); -await d1Database - .prepare("INSERT INTO entries (key, value) VALUES (?1, ?2)") - .bind("a", "1") - .run(); - -await mf.dispose(); -``` diff --git a/packages/miniflare/test/fixtures/migrations/3.20230821.0/d1/DATABASE/db.sqlite b/packages/miniflare/test/fixtures/migrations/3.20230821.0/d1/DATABASE/db.sqlite deleted file mode 100644 index cb44f6282aa79ab12ab630147fd7049404a14937..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI#K}*9h6u|M9bPg3pyzS6SjtVLazkpSG5GT0WgFBUq5vp~=tt*P3(3>Ae@nSl< z^X}#U$m8WDB#_@Ncaw2ZIr}tUyylfXlLOIO&aD*@?KAMX3FRlfP2+3RZwoD_&));{ zBihtrYV)=20Q(3afB*srAb6e#H+00R#|0009ILKmY**5I_I{1iB`$qXJ{{(Y}9|GJoO+ dfdB#sAb zFW?*a20o602d7qP5M1^q$oIz#!^|(4KcCyox3g|81NqE@qLq_?1t&%qQb0Pm7tg{eI961KQ~w1n2biU+77%n60lS+?jvU z)~hwO>`d}_D2j8ecA!746Slv0gW^oIUZ(V?_)Ej!Jfx@n&T+dxphv-ASB=NRNvt-! z%=Jz}+WX$^E)uKd)y~7nXCxRt5cl?CKTyV$BTub)0AFlo4trzHH{3Fu|ytB)61!2gE80QHH(>3 zAN;R;^v(alf58WR^OP3crCSz#l6(&>wCB?D`}__hrEEN|cS1`1{(jF6iNsy!G8yg; zAsokL*qUeSs7$eq>Cp~*<&SNjx0&H?Z-1Me|HliFG>>s@nvW`U264wt2gL|ojrR$w4$dVr7E}DG-@q_v}$+j z21)5aN(I8oV6WS^J*(r9mhq%T8V}jJS+A3(anEQPja6gg)ZlnPP#O;`FAonid^fv; zo#2a^=Agvd%VaGIwmFnC8)nO_)$8q~(pZa6_o!*E*P3nez-W^{u`iQjVV4VK zBbO_-C7$c-dh}KByvr_tEPD|4hx=Aa7OYB2W&ZGBDqmDp{^&+Z#*+k`9G6s1Uy${r zOM2dqg%`&=b{oxzxOyuL_pBbviD!p)8oN6P!ahsN1zm#BX4i!+v(aYrBx0^@)>}jv z&z3^joX!=Ws+VINah!rosV6vGx!Jj@-P?oc zQDAZL^S?N@pg;fu5P$##AOHafKmY;|fB*y_Flhq0sl}`8$BF3vKl{@^3Irek0SG_< z0uX=z1Rwwb2tWV=6D$zj|Ht+J1Sc4wK>z{}fB*y_009U<00Izz00fu-?*C&NKmY;| ufB*y_009U<00Izz00bsq0QdhVKgWm=0uX=z1Rwwb2tWV=5P$##An+T$0=WVJ diff --git a/packages/miniflare/test/plugins/d1/index.spec.ts b/packages/miniflare/test/plugins/d1/index.spec.ts index 3480e4c321d..f46edddd8d8 100644 --- a/packages/miniflare/test/plugins/d1/index.spec.ts +++ b/packages/miniflare/test/plugins/d1/index.spec.ts @@ -1,8 +1,3 @@ -import fs from "node:fs/promises"; -import path from "node:path"; -import { Miniflare } from "miniflare"; -import { test } from "vitest"; -import { FIXTURES_PATH, useDispose, useTmp } from "../../test-shared"; // Import suite tests - this registers the tests with vitest import "./suite"; import { setupTest } from "./test"; @@ -10,24 +5,3 @@ import { setupTest } from "./test"; // Post-wrangler 3.3, D1 bindings work directly, so use the input file // from the fixture, and no prefix on the binding name setupTest("DB", "worker.mjs", (mf) => mf.getD1Database("DB")); - -test("migrates database to new location", async ({ expect }) => { - // Copy legacy data to temporary directory - const tmp = await useTmp(); - const persistFixture = path.join(FIXTURES_PATH, "migrations", "3.20230821.0"); - const d1Persist = path.join(tmp, "d1"); - await fs.cp(path.join(persistFixture, "d1"), d1Persist, { recursive: true }); - - // Implicitly migrate data - const mf = new Miniflare({ - modules: true, - script: "", - d1Databases: ["DATABASE"], - d1Persist, - }); - useDispose(mf); - - const database = await mf.getD1Database("DATABASE"); - const { results } = await database.prepare("SELECT * FROM entries").all(); - expect(results).toEqual([{ key: "a", value: "1" }]); -}); diff --git a/packages/miniflare/test/plugins/kv/index.spec.ts b/packages/miniflare/test/plugins/kv/index.spec.ts index bb16aed11ce..de91b1741ac 100644 --- a/packages/miniflare/test/plugins/kv/index.spec.ts +++ b/packages/miniflare/test/plugins/kv/index.spec.ts @@ -1,13 +1,11 @@ import assert from "node:assert"; import { Blob } from "node:buffer"; import fs from "node:fs/promises"; -import path from "node:path"; import consumers from "node:stream/consumers"; import { KV_PLUGIN_NAME, MAX_BULK_GET_KEYS, Miniflare } from "miniflare"; import { beforeEach, type ExpectStatic, test } from "vitest"; import { createJunkStream, - FIXTURES_PATH, MiniflareDurableObjectControlStub, miniflareTest, namespace, @@ -811,26 +809,6 @@ test("persists on file-system", async ({ expect }) => { expect(await kv.get("key")).toBe("value"); }); -test("migrates database to new location", async ({ expect }) => { - // Copy legacy data to temporary directory - const tmp = await useTmp(); - const persistFixture = path.join(FIXTURES_PATH, "migrations", "3.20230821.0"); - const kvPersist = path.join(tmp, "kv"); - await fs.cp(path.join(persistFixture, "kv"), kvPersist, { recursive: true }); - - // Implicitly migrate data - const mf = new Miniflare({ - modules: true, - script: "", - kvNamespaces: ["NAMESPACE"], - kvPersist, - }); - useDispose(mf); - - const namespace = await mf.getKVNamespace("NAMESPACE"); - expect(await namespace.get("key")).toBe("value"); -}); - test("sticky blobs never deleted", async ({ expect }) => { // Checking regular behaviour that old blobs deleted in `put: overrides // existing keys` test. Only testing sticky blobs for KV, as the blob store diff --git a/packages/miniflare/test/plugins/r2/index.spec.ts b/packages/miniflare/test/plugins/r2/index.spec.ts index 077f1b6f1b3..c48ff14dcf6 100644 --- a/packages/miniflare/test/plugins/r2/index.spec.ts +++ b/packages/miniflare/test/plugins/r2/index.spec.ts @@ -3,12 +3,10 @@ import assert from "node:assert"; import crypto from "node:crypto"; import fs from "node:fs/promises"; -import path from "node:path"; import { text } from "node:stream/consumers"; import { Headers, Miniflare, R2_PLUGIN_NAME } from "miniflare"; import { beforeEach, type ExpectStatic, onTestFinished, test } from "vitest"; import { - FIXTURES_PATH, MiniflareDurableObjectControlStub, miniflareTest, namespace, @@ -1608,24 +1606,3 @@ test("list: is multipart aware", async ({ expect }) => { expect(object?.customMetadata).toEqual({ key: "value" }); expect(object?.httpMetadata).toEqual({ contentType: "text/plain" }); }); - -test("migrates database to new location", async ({ expect }) => { - // Copy legacy data to temporary directory - const tmp = await useTmp(); - const persistFixture = path.join(FIXTURES_PATH, "migrations", "3.20230821.0"); - const r2Persist = path.join(tmp, "r2"); - await fs.cp(path.join(persistFixture, "r2"), r2Persist, { recursive: true }); - - // Implicitly migrate data - const mf = new Miniflare({ - modules: true, - script: "", - r2Buckets: ["BUCKET"], - r2Persist, - }); - useDispose(mf); - - const bucket = await mf.getR2Bucket("BUCKET"); - const object = await bucket.get("key"); - expect(await object?.text()).toBe("value"); -}); diff --git a/packages/miniflare/turbo.json b/packages/miniflare/turbo.json index 2da61e9c5b0..8f6e6089f2c 100644 --- a/packages/miniflare/turbo.json +++ b/packages/miniflare/turbo.json @@ -4,7 +4,7 @@ "tasks": { "build": { "inputs": ["$TURBO_DEFAULT$", "!test/**"], - "outputs": ["dist/**", "bootstrap.js", "worker-metafiles/**"], + "outputs": ["dist/**", "worker-metafiles/**"], "env": ["CI_OS", "SPARROW_SOURCE_KEY"] }, "test:ci": { diff --git a/tools/deployments/validate-changesets.ts b/tools/deployments/validate-changesets.ts index 53683b821c4..498072b819c 100644 --- a/tools/deployments/validate-changesets.ts +++ b/tools/deployments/validate-changesets.ts @@ -31,7 +31,12 @@ export function validateChangesets( ); } - if (release.type === "major" && targetPackage?.private !== true) { + // TODO(miniflare-v5): restore this check after miniflare v5 is released + if ( + release.type === "major" && + targetPackage?.private !== true && + release.name !== "miniflare" + ) { errors.push( `Major version bumps are not allowed for package "${release.name}" in changeset at "${file}".` ); From 442c401ef5fb90765e116aaaf3b22c042ca6f4d3 Mon Sep 17 00:00:00 2001 From: emily-shen <69125074+emily-shen@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:00:23 +0100 Subject: [PATCH 2/6] clean up local testing paths in miniflare v5 (#14599) --- .changeset/drop-live-reload-endpoint.md | 7 ++ .changeset/remap-local-testing-paths.md | 17 +++++ .../rewrite-legacy-local-testing-paths.md | 18 +++++ fixtures/worker-app/src/index.js | 5 +- .../worker-with-resources/tests/index.test.ts | 2 +- .../local-explorer-ui/src/__e2e__/utils.ts | 33 +++++---- .../src/__tests__/utils/agent-prompt.test.ts | 21 +++--- .../src/components/Sidebar.tsx | 3 +- packages/local-explorer-ui/src/constants.ts | 9 +++ .../src/routes/r2/$bucketName/index.tsx | 3 +- .../src/routes/r2/$bucketName/object.$.tsx | 3 +- packages/local-explorer-ui/tsconfig.e2e.json | 3 +- packages/local-explorer-ui/vite.config.ts | 10 ++- packages/miniflare/scripts/filter-openapi.ts | 2 +- packages/miniflare/src/index.ts | 31 ++------- packages/miniflare/src/plugins/core/index.ts | 36 +--------- .../src/workers/assets/rpc-proxy.worker.ts | 2 +- .../miniflare/src/workers/core/constants.ts | 30 ++++---- .../workers/core/dev-registry-proxy.worker.ts | 3 +- .../src/workers/core/entry.worker.ts | 67 +----------------- .../src/workers/images/images.worker.ts | 2 +- .../local-explorer/generated/types.gen.ts | 2 +- .../workers/local-explorer/openapi.local.json | 2 +- .../src/workers/local-explorer/route-names.ts | 4 +- .../src/workers/stream/binding.worker.ts | 2 +- .../miniflare/src/workers/stream/schemas.ts | 4 +- packages/miniflare/test/index.spec.ts | 47 +++++-------- .../test/plugins/email/index.spec.ts | 26 +++---- .../test/plugins/images/index.spec.ts | 8 +-- .../test/plugins/local-explorer/index.spec.ts | 28 +++++--- .../plugins/local-explorer/telemetry.spec.ts | 12 ++-- .../test/plugins/stream/index.spec.ts | 12 ++-- .../bindings/__tests__/worker.spec.ts | 4 +- .../__tests__/cron-triggers.spec.ts | 19 +++--- .../__tests__/email-triggers.spec.ts | 68 ++++++++++--------- .../stream-binding/__tests__/worker.spec.ts | 2 +- .../src/__tests__/shortcuts.spec.ts | 4 +- .../src/plugins/preview.ts | 9 ++- .../src/plugins/trigger-handlers.ts | 51 ++++++++++++-- .../src/environment-variables/factory.ts | 2 +- .../environment-variables/misc-variables.ts | 2 +- .../wrangler/e2e/createTestHarness.test.ts | 4 +- packages/wrangler/e2e/dev.test.ts | 53 ++++++++------- packages/wrangler/e2e/multiworker-dev.test.ts | 2 +- .../wrangler/src/__tests__/workflows.test.ts | 4 +- .../startDevWorker/LocalRuntimeController.ts | 3 - .../src/api/startDevWorker/ProxyController.ts | 1 - packages/wrangler/src/api/test-harness.ts | 2 +- packages/wrangler/src/dev/miniflare/index.ts | 2 - packages/wrangler/src/dev/start-dev.ts | 2 +- packages/wrangler/src/workflows/local.ts | 2 +- .../templates/new-worker-scheduled.js | 2 +- .../templates/new-worker-scheduled.ts | 2 +- .../templates/startDevWorker/ProxyWorker.ts | 27 ++++++++ 54 files changed, 371 insertions(+), 350 deletions(-) create mode 100644 .changeset/drop-live-reload-endpoint.md create mode 100644 .changeset/remap-local-testing-paths.md create mode 100644 .changeset/rewrite-legacy-local-testing-paths.md create mode 100644 packages/local-explorer-ui/src/constants.ts diff --git a/.changeset/drop-live-reload-endpoint.md b/.changeset/drop-live-reload-endpoint.md new file mode 100644 index 00000000000..2112cc607c9 --- /dev/null +++ b/.changeset/drop-live-reload-endpoint.md @@ -0,0 +1,7 @@ +--- +"miniflare": major +--- + +Drop `/cdn-cgi/mf/reload` live reload endpoint and `liveReload` option + +The built-in live reload mechanism has been removed from Miniflare. This included a WebSocket endpoint at `/cdn-cgi/mf/reload`, the `liveReload` option, and the automatic injection of a live reload ``; - export const SCRIPT_CUSTOM_FETCH_SERVICE = `addEventListener("fetch", (event) => { const request = new Request(event.request); request.headers.set("${CoreHeaders.CUSTOM_FETCH_SERVICE}", ${CoreBindings.TEXT_CUSTOM_SERVICE}); @@ -1072,7 +1050,6 @@ export interface GlobalServicesOptions { sharedOptions: z.infer; allWorkerRoutes: Map; fallbackWorkerName: string | undefined; - loopbackPort: number; tmpPath: string; log: Log; /** All user workerd-native bindings, used for Miniflare's magic proxy and the local explorer worker */ @@ -1088,7 +1065,6 @@ export function getGlobalServices({ sharedOptions, allWorkerRoutes, fallbackWorkerName, - loopbackPort, tmpPath, log, proxyBindings, @@ -1203,14 +1179,6 @@ export function getGlobalServices({ data: encoder.encode(sharedOptions.unsafeProxySharedSecret), }); } - if (sharedOptions.liveReload) { - const liveReloadScript = LIVE_RELOAD_SCRIPT_TEMPLATE(loopbackPort); - serviceEntryBindings.push({ - name: CoreBindings.DATA_LIVE_RELOAD_SCRIPT, - data: encoder.encode(liveReloadScript), - }); - } - const services: Service[] = [ { name: SERVICE_LOOPBACK, diff --git a/packages/miniflare/src/workers/assets/rpc-proxy.worker.ts b/packages/miniflare/src/workers/assets/rpc-proxy.worker.ts index 8ed03b60098..e2d36a77442 100644 --- a/packages/miniflare/src/workers/assets/rpc-proxy.worker.ts +++ b/packages/miniflare/src/workers/assets/rpc-proxy.worker.ts @@ -30,7 +30,7 @@ export default class RPCProxyWorker extends WorkerEntrypoint { // any scheduled logic; it just dispatches a real scheduled event to the user // worker via the Fetcher built-in, then propagates the user worker's noRetry // decision back onto this controller so the outcome surfaces correctly to - // the caller (e.g. the entry worker's `/cdn-cgi/handler/scheduled` handler). + // the caller (e.g. the entry worker's `/cdn-cgi/local/scheduled` handler). async scheduled(controller: ScheduledController) { const result = await this.env.USER_WORKER.scheduled?.({ cron: controller.cron, diff --git a/packages/miniflare/src/workers/core/constants.ts b/packages/miniflare/src/workers/core/constants.ts index 79a397917c9..a72c648eccb 100644 --- a/packages/miniflare/src/workers/core/constants.ts +++ b/packages/miniflare/src/workers/core/constants.ts @@ -1,26 +1,23 @@ /** - * Reserved `/cdn-cgi/` paths for internal Miniflare endpoints. - * These paths are reserved by Cloudflare's network and won't conflict with user routes. + * Reserved paths for internal Miniflare endpoints. + * + * Paths under `/cdn-cgi/local/` are reserved by Cloudflare's network + * and won't conflict with user routes. Paths under `/__cf_local/` live + * outside `/cdn-cgi/` so they remain reachable over tunnels. */ export const CorePaths = { /** Magic proxy used by getPlatformProxy */ - PLATFORM_PROXY: "/cdn-cgi/platform-proxy", + PLATFORM_PROXY: "/cdn-cgi/local/platform-proxy", /** Trigger scheduled event handlers */ - SCHEDULED: "/cdn-cgi/handler/scheduled", + SCHEDULED: "/cdn-cgi/local/scheduled", /** Trigger email event handlers */ - EMAIL: "/cdn-cgi/handler/email", - /** Handler path prefix for validation */ - HANDLER_PREFIX: "/cdn-cgi/handler/", - /** Live reload WebSocket endpoint */ - LIVE_RELOAD: "/cdn-cgi/mf/reload", + EMAIL: "/cdn-cgi/local/email", /** Local explorer UI and API */ - EXPLORER: "/cdn-cgi/explorer", - /** Legacy way to trigger scheduled event handlers */ - LEGACY_SCHEDULED: "/cdn-cgi/mf/scheduled", - /** Stream video serving endpoint */ - STREAM_VIDEO: "/cdn-cgi/mf/stream", - /** Local image delivery endpoint for serving hosted images */ - IMAGE_DELIVERY: "/cdn-cgi/mf/imagedelivery", + EXPLORER: "/cdn-cgi/local/explorer", + /** Stream video serving endpoint (outside /cdn-cgi/ for tunnel access) */ + STREAM_VIDEO: "/__cf_local/stream", + /** Local image delivery endpoint (outside /cdn-cgi/ for tunnel access) */ + IMAGE_DELIVERY: "/__cf_local/imagedelivery", /** Public R2 bucket object serving endpoint */ R2_PUBLIC: "/cdn-cgi/local/r2/public", } as const; @@ -75,7 +72,6 @@ export const CoreBindings = { JSON_CF_BLOB: "CF_BLOB", JSON_ROUTES: "MINIFLARE_ROUTES", JSON_LOG_LEVEL: "MINIFLARE_LOG_LEVEL", - DATA_LIVE_RELOAD_SCRIPT: "MINIFLARE_LIVE_RELOAD_SCRIPT", DURABLE_OBJECT_NAMESPACE_PROXY: "MINIFLARE_PROXY", DATA_PROXY_SECRET: "MINIFLARE_PROXY_SECRET", DATA_PROXY_SHARED_SECRET: "MINIFLARE_PROXY_SHARED_SECRET", diff --git a/packages/miniflare/src/workers/core/dev-registry-proxy.worker.ts b/packages/miniflare/src/workers/core/dev-registry-proxy.worker.ts index 392f46bd9e3..8fc05d10d87 100644 --- a/packages/miniflare/src/workers/core/dev-registry-proxy.worker.ts +++ b/packages/miniflare/src/workers/core/dev-registry-proxy.worker.ts @@ -1,5 +1,6 @@ import { WorkerEntrypoint } from "cloudflare:workers"; import { getQueueServiceName, HEADER_QUEUE_NAME } from "../queues/constants"; +import { CorePaths } from "./constants"; import { findQueueConsumer, resolveTarget, @@ -145,7 +146,7 @@ export class ExternalServiceProxy extends WorkerEntrypoint { params.set("time", String(controller.scheduledTime)); } const response = await this._entryFetcher.fetch( - new Request(`http://localhost/cdn-cgi/handler/scheduled?${params}`, { + new Request(`http://localhost${CorePaths.SCHEDULED}?${params}`, { headers: { "MF-Route-Override": this.ctx.props.service }, }) ); diff --git a/packages/miniflare/src/workers/core/entry.worker.ts b/packages/miniflare/src/workers/core/entry.worker.ts index bcea7b82079..673100f8ce6 100644 --- a/packages/miniflare/src/workers/core/entry.worker.ts +++ b/packages/miniflare/src/workers/core/entry.worker.ts @@ -26,7 +26,6 @@ type Env = { [CoreBindings.JSON_CF_BLOB]: IncomingRequestCfProperties; [CoreBindings.JSON_ROUTES]: WorkerRoute[]; [CoreBindings.JSON_LOG_LEVEL]: LogLevel; - [CoreBindings.DATA_LIVE_RELOAD_SCRIPT]?: ArrayBuffer; [CoreBindings.DURABLE_OBJECT_NAMESPACE_PROXY]: DurableObjectNamespace; [CoreBindings.DATA_PROXY_SHARED_SECRET]?: ArrayBuffer; [CoreBindings.TRIGGER_HANDLERS]: boolean; @@ -294,45 +293,6 @@ function maybePrettifyError(request: Request, response: Response, env: Env) { ); } -function maybeInjectLiveReload( - response: Response, - env: Env, - ctx: ExecutionContext -) { - const liveReloadScript = env[CoreBindings.DATA_LIVE_RELOAD_SCRIPT]; - if ( - liveReloadScript === undefined || - !response.headers.get("Content-Type")?.toLowerCase().includes("text/html") - ) { - return response; - } - - const headers = new Headers(response.headers); - const contentLength = parseInt(headers.get("content-length") ?? "NaN"); - if (!isNaN(contentLength)) { - headers.set( - "content-length", - String(contentLength + liveReloadScript.byteLength) - ); - } - - const { readable, writable } = new IdentityTransformStream(); - ctx.waitUntil( - (async () => { - await response.body?.pipeTo(writable, { preventClose: true }); - const writer = writable.getWriter(); - await writer.write(liveReloadScript); - await writer.close(); - })() - ); - - return new Response(readable, { - status: response.status, - statusText: response.statusText, - headers, - }); -} - const acceptEncodingElement = /^(?[a-z]+|\*)(?:\s*;\s*q=(?\d+(?:.\d+)?))?$/; interface AcceptedEncoding { @@ -572,24 +532,7 @@ export default >{ return await imagesDelivery.fetch(request); } if (env[CoreBindings.TRIGGER_HANDLERS]) { - if ( - url.pathname === CorePaths.SCHEDULED || - /* legacy URL path */ url.pathname === CorePaths.LEGACY_SCHEDULED - ) { - if (url.pathname === CorePaths.LEGACY_SCHEDULED) { - ctx.waitUntil( - env[CoreBindings.SERVICE_LOOPBACK].fetch( - "http://localhost/core/log", - { - method: "POST", - headers: { - [SharedHeaders.LOG_LEVEL]: LogLevel.WARN.toString(), - }, - body: `Triggering scheduled handlers via a request to \`${CorePaths.LEGACY_SCHEDULED}\` is deprecated, and will be removed in a future version of Miniflare. Instead, send a request to \`${CorePaths.SCHEDULED}\``, - } - ) - ); - } + if (url.pathname === CorePaths.SCHEDULED) { return await handleScheduled(url.searchParams, service); } @@ -602,13 +545,6 @@ export default >{ ctx ); } - - if (url.pathname.startsWith(CorePaths.HANDLER_PREFIX)) { - return new Response( - `"${url.pathname}" is not a valid handler. Did you mean to use "${CorePaths.SCHEDULED}" or "${CorePaths.EMAIL}"?`, - { status: 404 } - ); - } } const streamService = env[CoreBindings.SERVICE_STREAM]; @@ -633,7 +569,6 @@ export default >{ if (!disablePrettyErrorPage) { response = await maybePrettifyError(request, response, env); } - response = maybeInjectLiveReload(response, env, ctx); response = ensureAcceptableEncoding(clientAcceptEncoding, response); if (env[CoreBindings.LOG_REQUESTS]) { response = maybeLogRequest(request, response, env, ctx, startTime); diff --git a/packages/miniflare/src/workers/images/images.worker.ts b/packages/miniflare/src/workers/images/images.worker.ts index 1f47dfaba81..5715b2af34e 100644 --- a/packages/miniflare/src/workers/images/images.worker.ts +++ b/packages/miniflare/src/workers/images/images.worker.ts @@ -251,7 +251,7 @@ export default class ImagesService extends WorkerEntrypoint { async fetch(request: Request): Promise { const url = new URL(request.url); - // Serve image bytes at /cdn-cgi/mf/imagedelivery// + // Serve image bytes at /__cf_local/imagedelivery// if (url.pathname.startsWith(`${CorePaths.IMAGE_DELIVERY}/`)) { const parts = url.pathname .slice(CorePaths.IMAGE_DELIVERY.length + 1) diff --git a/packages/miniflare/src/workers/local-explorer/generated/types.gen.ts b/packages/miniflare/src/workers/local-explorer/generated/types.gen.ts index 215f2aab522..7d76dd79135 100644 --- a/packages/miniflare/src/workers/local-explorer/generated/types.gen.ts +++ b/packages/miniflare/src/workers/local-explorer/generated/types.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts export type ClientOptions = { - baseUrl: `${string}://${string}/cdn-cgi/explorer/api` | (string & {}); + baseUrl: `${string}://${string}/cdn-cgi/local/explorer/api` | (string & {}); }; export type R2V4Response = { diff --git a/packages/miniflare/src/workers/local-explorer/openapi.local.json b/packages/miniflare/src/workers/local-explorer/openapi.local.json index 47357fcdbb0..310013567b4 100644 --- a/packages/miniflare/src/workers/local-explorer/openapi.local.json +++ b/packages/miniflare/src/workers/local-explorer/openapi.local.json @@ -8,7 +8,7 @@ "servers": [ { "description": "Local Explorer", - "url": "/cdn-cgi/explorer/api" + "url": "/cdn-cgi/local/explorer/api" } ], "paths": { diff --git a/packages/miniflare/src/workers/local-explorer/route-names.ts b/packages/miniflare/src/workers/local-explorer/route-names.ts index f6afe51f55b..c5a21a7805f 100644 --- a/packages/miniflare/src/workers/local-explorer/route-names.ts +++ b/packages/miniflare/src/workers/local-explorer/route-names.ts @@ -36,8 +36,8 @@ const ROUTE_PATTERNS: [RegExp, string][] = [ * Strips IDs and converts to dot notation. */ export function getRouteName(path: string): string { - // Remove /cdn-cgi/explorer/api prefix - const apiPath = path.replace(/^\/cdn-cgi\/explorer\/api/, ""); + // Remove /cdn-cgi/local/explorer/api prefix + const apiPath = path.replace(/^\/cdn-cgi\/local\/explorer\/api/, ""); for (const [pattern, name] of ROUTE_PATTERNS) { if (pattern.test(apiPath)) { diff --git a/packages/miniflare/src/workers/stream/binding.worker.ts b/packages/miniflare/src/workers/stream/binding.worker.ts index aea82466120..739f8292e3f 100644 --- a/packages/miniflare/src/workers/stream/binding.worker.ts +++ b/packages/miniflare/src/workers/stream/binding.worker.ts @@ -32,7 +32,7 @@ function rowsToDownloadResponse( export class StreamBinding extends WorkerEntrypoint { async fetch(request: Request): Promise { const url = new URL(request.url); - const match = url.pathname.match(/^\/cdn-cgi\/mf\/stream\/([^/]+)\/watch$/); + const match = url.pathname.match(/^\/__cf_local\/stream\/([^/]+)\/watch$/); if (!match) { return new Response("Not found", { status: 404 }); } diff --git a/packages/miniflare/src/workers/stream/schemas.ts b/packages/miniflare/src/workers/stream/schemas.ts index 61f09831a7b..e4fb90b76aa 100644 --- a/packages/miniflare/src/workers/stream/schemas.ts +++ b/packages/miniflare/src/workers/stream/schemas.ts @@ -1,3 +1,5 @@ +import { CorePaths } from "../core/constants"; + export const SQL_SCHEMA = ` CREATE TABLE IF NOT EXISTS _mf_stream_videos ( id TEXT PRIMARY KEY, @@ -130,7 +132,7 @@ export type DownloadRow = { export function rowToStreamVideo(row: VideoRow, entryUrl: URL): StreamVideo { const placeholderUrl = `https://customer-placeholder.cloudflarestream.com/${row.id}`; - const videoUrl = `${entryUrl.origin}/cdn-cgi/mf/stream/${row.id}/watch`; + const videoUrl = `${entryUrl.origin}${CorePaths.STREAM_VIDEO}/${row.id}/watch`; return { id: row.id, creator: row.creator, diff --git a/packages/miniflare/test/index.spec.ts b/packages/miniflare/test/index.spec.ts index 22372d107a8..0e76d185168 100644 --- a/packages/miniflare/test/index.spec.ts +++ b/packages/miniflare/test/index.spec.ts @@ -208,13 +208,9 @@ test("Miniflare: ready returns copy of entry URL", async ({ expect }) => { }); test("Miniflare: setOptions: can update host/port", async ({ expect }) => { - // Extract loopback port from injected live reload script - const loopbackPortRegexp = /\/\/ Miniflare Live Reload.+url\.port = (\d+)/s; - const opts: MiniflareOptions = { port: 0, inspectorPort: 0, - liveReload: true, script: `addEventListener("fetch", (event) => { event.respondWith(new Response("

👋

", { headers: { "Content-Type": "text/html;charset=utf-8" } @@ -227,9 +223,7 @@ test("Miniflare: setOptions: can update host/port", async ({ expect }) => { async function getState() { const url = await mf.ready; const inspectorUrl = await mf.getInspectorURL(); - const res = await mf.dispatchFetch("http://localhost"); - const loopbackPort = loopbackPortRegexp.exec(await res.text())?.[1]; - return { url, inspectorUrl, loopbackPort }; + return { url, inspectorUrl }; } const state1 = await getState(); @@ -243,19 +237,12 @@ test("Miniflare: setOptions: can update host/port", async ({ expect }) => { expect(state1.inspectorUrl.port).not.toBe("0"); expect(state1.inspectorUrl.port).toBe(state2.inspectorUrl.port); - // Make sure updating the host restarted the loopback server - expect(state1.loopbackPort).toBeDefined(); - expect(state2.loopbackPort).toBeDefined(); - expect(state1.loopbackPort).not.toBe(state2.loopbackPort); - - // Make sure setting port to `undefined` always gives a new port, but keeps - // existing loopback server + // Make sure setting port to `undefined` always gives a new port opts.port = undefined; await mf.setOptions(opts); const state3 = await getState(); expect(state3.url.port).not.toBe("0"); expect(state1.url.port).not.toBe(state3.url.port); - expect(state2.loopbackPort).toBe(state3.loopbackPort); }); const interfaces = os.networkInterfaces(); @@ -1724,11 +1711,11 @@ test("Miniflare: manually triggered scheduled events", async ({ expect }) => { let res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("false"); - res = await mf.dispatchFetch("http://localhost/cdn-cgi/handler/scheduled"); + res = await mf.dispatchFetch("http://localhost/cdn-cgi/local/scheduled"); expect(await res.text()).toBe("ok"); res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/scheduled?format=json" + "http://localhost/cdn-cgi/local/scheduled?format=json" ); expect(await res.json()).toEqual({ outcome: "ok", noRetry: true }); @@ -1795,7 +1782,7 @@ test("Miniflare: manually triggered scheduled events with assets", async ({ expect(res.headers.get("content-type")).toBe("text/markdown; charset=utf-8"); expect(await res.text()).toBe("asset"); - res = await mf.dispatchFetch("http://localhost/cdn-cgi/handler/scheduled"); + res = await mf.dispatchFetch("http://localhost/cdn-cgi/local/scheduled"); expect(await res.text()).toBe("ok"); res = await mf.dispatchFetch("http://localhost"); @@ -1805,7 +1792,7 @@ test("Miniflare: manually triggered scheduled events with assets", async ({ expect(json.scheduledTime).toBeDefined(); res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/scheduled?format=json&cron=0+0+0+0+0&time=1234567890987" + "http://localhost/cdn-cgi/local/scheduled?format=json&cron=0+0+0+0+0&time=1234567890987" ); expect(await res.json()).toEqual({ outcome: "ok", @@ -1845,7 +1832,7 @@ test("Miniflare: manually triggered email handler - valid email", async ({ expect(await res.text()).toBe("false"); res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com", + "http://localhost/cdn-cgi/local/email?from=someone@example.com&to=someone-else@example.com", { body: `From: someone To: someone else @@ -1892,7 +1879,7 @@ test("Miniflare: manually triggered email handler - setReject does not throw", a expect(await res.text()).toBe("false"); res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com", + "http://localhost/cdn-cgi/local/email?from=someone@example.com&to=someone-else@example.com", { body: `From: someone To: someone else @@ -1941,7 +1928,7 @@ test("Miniflare: manually triggered email handler - forward does not throw", asy expect(await res.text()).toBe("false"); res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com", + "http://localhost/cdn-cgi/local/email?from=someone@example.com&to=someone-else@example.com", { body: `From: someone To: someone else @@ -1987,7 +1974,7 @@ test("Miniflare: manually triggered email handler - invalid email, no message id expect(await res.text()).toBe("false"); res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com", + "http://localhost/cdn-cgi/local/email?from=someone@example.com&to=someone-else@example.com", { body: `From: someone To: someone else @@ -2050,7 +2037,7 @@ This is a random email body. expect(await res.text()).toBe("false"); res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com", + "http://localhost/cdn-cgi/local/email?from=someone@example.com&to=someone-else@example.com", { body: `From: someone To: someone else @@ -2070,7 +2057,7 @@ This is a random email body. expect(await res.text()).toBe("true"); }); -test("Miniflare: unimplemented /cdn-cgi/handler/ routes", async ({ +test("Miniflare: unrecognised /cdn-cgi/local/ routes fall through to user worker", async ({ expect, }) => { const mf = new Miniflare({ @@ -2086,11 +2073,9 @@ test("Miniflare: unimplemented /cdn-cgi/handler/ routes", async ({ }); useDispose(mf); - const res = await mf.dispatchFetch("http://localhost/cdn-cgi/handler/foo"); - expect(await res.text()).toBe( - `"/cdn-cgi/handler/foo" is not a valid handler. Did you mean to use "/cdn-cgi/handler/scheduled" or "/cdn-cgi/handler/email"?` - ); - expect(res.status).toBe(404); + const res = await mf.dispatchFetch("http://localhost/cdn-cgi/local/foo"); + expect(await res.text()).toBe("Hello world"); + expect(res.status).toBe(200); }); test("Miniflare: other /cdn-cgi/ routes", async ({ expect }) => { @@ -2150,7 +2135,7 @@ test("Miniflare: blocks non-local Host headers from reaching /cdn-cgi/ routes", setHost: false, headers: { Host: "example.trycloudflare.com", - "MF-Original-URL": "http://localhost/cdn-cgi/handler/scheduled", + "MF-Original-URL": "http://localhost/cdn-cgi/local/scheduled", }, }, (res) => { diff --git a/packages/miniflare/test/plugins/email/index.spec.ts b/packages/miniflare/test/plugins/email/index.spec.ts index e89eca93e3e..6f11cc48cff 100644 --- a/packages/miniflare/test/plugins/email/index.spec.ts +++ b/packages/miniflare/test/plugins/email/index.spec.ts @@ -478,7 +478,7 @@ test("reply validation: x-auto-response-suppress", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -518,7 +518,7 @@ test("reply validation: Auto-Submitted", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -558,7 +558,7 @@ test("reply validation: only In-Reply-To", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -598,7 +598,7 @@ test("reply validation: only References", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -639,7 +639,7 @@ test("reply validation: >100 References", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -682,7 +682,7 @@ test("reply: mismatched From: header", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -722,7 +722,7 @@ test("reply: unparseable", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -770,7 +770,7 @@ test("reply: no message id", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -820,7 +820,7 @@ test("reply: disallowed header", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -869,7 +869,7 @@ test("reply: missing In-Reply-To", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -921,7 +921,7 @@ test("reply: wrong In-Reply-To", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -976,7 +976,7 @@ test("reply: invalid references", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -1025,7 +1025,7 @@ test("reply: references generated correctly", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", diff --git a/packages/miniflare/test/plugins/images/index.spec.ts b/packages/miniflare/test/plugins/images/index.spec.ts index 178d2e3f215..2a210502850 100644 --- a/packages/miniflare/test/plugins/images/index.spec.ts +++ b/packages/miniflare/test/plugins/images/index.spec.ts @@ -93,7 +93,7 @@ function upload( } describe("Images local delivery", () => { - test("variant URLs are absolute and use /cdn-cgi/mf/imagedelivery/ path", async ({ + test("variant URLs are absolute and use /__cf_local/imagedelivery/ path", async ({ expect, }) => { const mf = createMiniflare(); @@ -103,7 +103,7 @@ describe("Images local delivery", () => { const metadata = await upload(mf, TEST_IMAGE_BYTES, { id: "variant-test" }); expect(metadata.variants).toHaveLength(1); expect(metadata.variants[0]).toBe( - `${url.origin}/cdn-cgi/mf/imagedelivery/variant-test/public` + `${url.origin}/__cf_local/imagedelivery/variant-test/public` ); }); @@ -115,7 +115,7 @@ describe("Images local delivery", () => { await upload(mf, TEST_IMAGE_BYTES, { id: "delivery-test" }); const response = await mf.dispatchFetch( - `${url.origin}/cdn-cgi/mf/imagedelivery/delivery-test/public` + `${url.origin}/__cf_local/imagedelivery/delivery-test/public` ); expect(response.status).toBe(200); const data = new Uint8Array(await response.arrayBuffer()); @@ -130,7 +130,7 @@ describe("Images local delivery", () => { const url = await mf.ready; const response = await mf.dispatchFetch( - `${url.origin}/cdn-cgi/mf/imagedelivery/does-not-exist/public` + `${url.origin}/__cf_local/imagedelivery/does-not-exist/public` ); expect(response.status).toBe(404); await response.arrayBuffer(); diff --git a/packages/miniflare/test/plugins/local-explorer/index.spec.ts b/packages/miniflare/test/plugins/local-explorer/index.spec.ts index 2b492d4f8d7..5a3941d6af1 100644 --- a/packages/miniflare/test/plugins/local-explorer/index.spec.ts +++ b/packages/miniflare/test/plugins/local-explorer/index.spec.ts @@ -259,9 +259,11 @@ describe("Local Explorer API validation", () => { }); describe("routing", () => { - test("serves OpenAPI spec at /cdn-cgi/explorer/api", async ({ expect }) => { + test("serves OpenAPI spec at /cdn-cgi/local/explorer/api", async ({ + expect, + }) => { const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/explorer/api" + "http://localhost/cdn-cgi/local/explorer/api" ); expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toContain("application/json"); @@ -273,28 +275,36 @@ describe("Local Explorer API validation", () => { }); }); - test("serves explorer UI at /cdn-cgi/explorer", async ({ expect }) => { - const res = await mf.dispatchFetch("http://localhost/cdn-cgi/explorer"); + test("serves explorer UI at /cdn-cgi/local/explorer", async ({ + expect, + }) => { + const res = await mf.dispatchFetch( + "http://localhost/cdn-cgi/local/explorer" + ); expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toContain("text/html"); await res.arrayBuffer(); // Drain }); - test("serves explorer UI at /cdn-cgi/explorer/", async ({ expect }) => { - const res = await mf.dispatchFetch("http://localhost/cdn-cgi/explorer/"); + test("serves explorer UI at /cdn-cgi/local/explorer/", async ({ + expect, + }) => { + const res = await mf.dispatchFetch( + "http://localhost/cdn-cgi/local/explorer/" + ); expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toContain("text/html"); await res.arrayBuffer(); // Drain }); - test("does not match paths that start with /cdn-cgi/explorer but are not the explorer", async ({ + test("does not match paths that start with /cdn-cgi/local/explorer but are not the explorer", async ({ expect, }) => { - // This should fall through to the user worker, not match the explorer + // /cdn-cgi/local/explorerfoo falls through to the user worker const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/explorerfoo" + "http://localhost/cdn-cgi/local/explorerfoo" ); expect(res.status).toBe(200); expect(await res.text()).toBe("user worker"); diff --git a/packages/miniflare/test/plugins/local-explorer/telemetry.spec.ts b/packages/miniflare/test/plugins/local-explorer/telemetry.spec.ts index 501a0c8ca38..6f2b28ad7ac 100644 --- a/packages/miniflare/test/plugins/local-explorer/telemetry.spec.ts +++ b/packages/miniflare/test/plugins/local-explorer/telemetry.spec.ts @@ -15,7 +15,7 @@ describe("getRouteName", () => { test(`handles ${method.toUpperCase()} ${path}`, ({ expect }) => { // Replace {param} placeholders with dummy values const testPath = path.replace(/\{[^}]+\}/g, "test-id"); - const fullPath = `/cdn-cgi/explorer/api${testPath}`; + const fullPath = `/cdn-cgi/local/explorer/api${testPath}`; const routeName = getRouteName(fullPath); @@ -27,12 +27,14 @@ describe("getRouteName", () => { }); test("maps routes to expected names", ({ expect }) => { - expect(getRouteName(`/cdn-cgi/explorer/api/storage/kv/namespaces`)).toBe( - "kv.namespaces" - ); + expect( + getRouteName(`/cdn-cgi/local/explorer/api/storage/kv/namespaces`) + ).toBe("kv.namespaces"); }); test("returns unknown for unrecognized paths", ({ expect }) => { - expect(getRouteName("/cdn-cgi/explorer/api/unknown/path")).toBe("unknown"); + expect(getRouteName("/cdn-cgi/local/explorer/api/unknown/path")).toBe( + "unknown" + ); }); }); diff --git a/packages/miniflare/test/plugins/stream/index.spec.ts b/packages/miniflare/test/plugins/stream/index.spec.ts index 33ed4a1a9aa..202c2a94dbc 100644 --- a/packages/miniflare/test/plugins/stream/index.spec.ts +++ b/packages/miniflare/test/plugins/stream/index.spec.ts @@ -211,7 +211,7 @@ describe("Stream videos", () => { expect(video.hlsPlaybackUrl).toContain(video.id); expect(video.dashPlaybackUrl).toContain(video.id); expect(video.preview).toMatch( - new RegExp(`^http://.*?/cdn-cgi/mf/stream/${video.id}/watch$`) + new RegExp(`^http://.*?/__cf_local/stream/${video.id}/watch$`) ); const details = (await sendCmdToWorker(mf, "video.details", { @@ -646,7 +646,7 @@ describe("Stream videos list", () => { }); describe("Stream video serving", () => { - test("serve video via /cdn-cgi/mf/stream/:id/watch", async ({ expect }) => { + test("serve video via /__cf_local/stream/:id/watch", async ({ expect }) => { const mf = createMiniflare(); useDispose(mf); const { http: videoUrl } = await useServer( @@ -659,7 +659,7 @@ describe("Stream video serving", () => { // Fetch the video via the preview URL path and consume body immediately const resp = await mf.dispatchFetch( - `http://placeholder/cdn-cgi/mf/stream/${video.id}/watch` + `http://placeholder/__cf_local/stream/${video.id}/watch` ); const bytes = new Uint8Array(await resp.arrayBuffer()); expect(resp.status).toBe(200); @@ -671,7 +671,7 @@ describe("Stream video serving", () => { useDispose(mf); const resp = await mf.dispatchFetch( - "http://placeholder/cdn-cgi/mf/stream/00000000-0000-0000-0000-000000000000/watch" + "http://placeholder/__cf_local/stream/00000000-0000-0000-0000-000000000000/watch" ); await resp.arrayBuffer(); // consume body to avoid dispatchFetch error expect(resp.status).toBe(404); @@ -1626,7 +1626,7 @@ describe("Stream publicUrl", () => { })) as Video; expect(video.preview).toBe( - `http://my-proxy.example.com:8080/cdn-cgi/mf/stream/${video.id}/watch` + `http://my-proxy.example.com:8080/__cf_local/stream/${video.id}/watch` ); }); @@ -1647,7 +1647,7 @@ describe("Stream publicUrl", () => { // (http://127.0.0.1:) rather than any external proxy URL expect(video.preview).toMatch( new RegExp( - `^http://127\\.0\\.0\\.1:\\d+/cdn-cgi/mf/stream/${video.id}/watch$` + `^http://127\\.0\\.0\\.1:\\d+/__cf_local/stream/${video.id}/watch$` ) ); }); diff --git a/packages/vite-plugin-cloudflare/playground/bindings/__tests__/worker.spec.ts b/packages/vite-plugin-cloudflare/playground/bindings/__tests__/worker.spec.ts index 3615cdba070..266afd06483 100644 --- a/packages/vite-plugin-cloudflare/playground/bindings/__tests__/worker.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/bindings/__tests__/worker.spec.ts @@ -2,7 +2,9 @@ import { test } from "vitest"; import { getResponse, getTextResponse } from "../../__test-utils__"; test("serves Local Explorer UI", async ({ expect }) => { - const response = await getResponse("/cdn-cgi/explorer"); + let response = await getResponse("/cdn-cgi/local/explorer"); + expect(response.status()).toBe(200); + response = await getResponse("/cdn-cgi/explorer"); expect(response.status()).toBe(200); }); diff --git a/packages/vite-plugin-cloudflare/playground/cron-triggers/__tests__/cron-triggers.spec.ts b/packages/vite-plugin-cloudflare/playground/cron-triggers/__tests__/cron-triggers.spec.ts index 5b940cd0e13..83aee80d067 100644 --- a/packages/vite-plugin-cloudflare/playground/cron-triggers/__tests__/cron-triggers.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/cron-triggers/__tests__/cron-triggers.spec.ts @@ -1,10 +1,13 @@ -import { test } from "vitest"; +import { describe, test } from "vitest"; import { getTextResponse, serverLogs } from "../../__test-utils__"; -test("Supports testing Cron Triggers at '/cdn-cgi/handler/scheduled' route", async ({ - expect, -}) => { - const cronResponse = await getTextResponse("/cdn-cgi/handler/scheduled"); - expect(cronResponse).toBe("ok"); - expect(serverLogs.info.join()).toContain("Cron processed"); -}); +describe.each(["/cdn-cgi/local/scheduled", "/cdn-cgi/handler/scheduled"])( + "%s", + (path) => { + test("Supports testing Cron Triggers", async ({ expect }) => { + const cronResponse = await getTextResponse(path); + expect(cronResponse).toBe("ok"); + expect(serverLogs.info.join()).toContain("Cron processed"); + }); + } +); diff --git a/packages/vite-plugin-cloudflare/playground/email-worker/__tests__/email-triggers.spec.ts b/packages/vite-plugin-cloudflare/playground/email-worker/__tests__/email-triggers.spec.ts index 76600cffb55..613c596acf4 100644 --- a/packages/vite-plugin-cloudflare/playground/email-worker/__tests__/email-triggers.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/email-worker/__tests__/email-triggers.spec.ts @@ -1,7 +1,7 @@ import { readFileSync } from "node:fs"; import path from "node:path"; import dedent from "ts-dedent"; -import { test, vi } from "vitest"; +import { describe, test, vi } from "vitest"; import { getTextResponse, rootDir, @@ -16,41 +16,43 @@ test("Supports sending email via the email binding", async ({ expect }) => { expect(sendEmailResponse).toBe("Email message sent successfully!"); }); -test("Supports testing Email Workers at '/cdn-cgi/handler/scheduled' route", async ({ - expect, -}) => { - const params = new URLSearchParams(); - params.append("from", "sender@example.com"); - params.append("to", "recipient@example.com"); +// The canonical path is `/cdn-cgi/local/email`; `/cdn-cgi/handler/email` is the +// legacy path kept working via a rewrite in the trigger-handlers plugin. +describe.each(["/cdn-cgi/local/email", "/cdn-cgi/handler/email"])( + "%s", + (path) => { + test("Supports testing Email Workers", async ({ expect }) => { + const params = new URLSearchParams(); + params.append("from", "sender@example.com"); + params.append("to", "recipient@example.com"); - const fetchResponse = await fetch( - `${viteTestUrl}/cdn-cgi/handler/email?${params}`, - { - method: "POST", - body: dedent` - From: "John" - Reply-To: sender@example.com - To: recipient@example.com - Subject: Testing Email Workers Local Dev - Content-Type: text/html; charset="windows-1252" - X-Mailer: Curl - Date: Tue, 27 Aug 2024 08:49:44 -0700 - Message-ID: <6114391943504294873000@ZSH-GHOSTTY> + const fetchResponse = await fetch(`${viteTestUrl}${path}?${params}`, { + method: "POST", + body: dedent` + From: "John" + Reply-To: sender@example.com + To: recipient@example.com + Subject: Testing Email Workers Local Dev + Content-Type: text/html; charset="windows-1252" + X-Mailer: Curl + Date: Tue, 27 Aug 2024 08:49:44 -0700 + Message-ID: <6114391943504294873000@ZSH-GHOSTTY> - Hi there - `, - } - ); + Hi there + `, + }); - const emailStdout = serverLogs.info.join(); - expect(await fetchResponse.text()).toBe( - "Worker successfully processed email" - ); - expect(emailStdout).toContain( - `Received email from sender@example.com on ${new Date(" 27 Aug 2024 08:49:44 -0700").toISOString()} with following message:` - ); - expect(emailStdout).toContain("Hi there"); -}); + const emailStdout = serverLogs.info.join(); + expect(await fetchResponse.text()).toBe( + "Worker successfully processed email" + ); + expect(emailStdout).toContain( + `Received email from sender@example.com on ${new Date(" 27 Aug 2024 08:49:44 -0700").toISOString()} with following message:` + ); + expect(emailStdout).toContain("Hi there"); + }); + } +); test("logs sent emails to a directory within the project directory", async ({ expect, diff --git a/packages/vite-plugin-cloudflare/playground/stream-binding/__tests__/worker.spec.ts b/packages/vite-plugin-cloudflare/playground/stream-binding/__tests__/worker.spec.ts index 2c851f27850..e8e86e3c9a0 100644 --- a/packages/vite-plugin-cloudflare/playground/stream-binding/__tests__/worker.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/stream-binding/__tests__/worker.spec.ts @@ -8,7 +8,7 @@ test("stream upload returns a valid preview URL", async ({ expect }) => { id: string; }; expect(result.id).toBeTruthy(); - expect(result.preview).toContain("/cdn-cgi/mf/stream/"); + expect(result.preview).toContain("/__cf_local/stream/"); expect(result.preview).toContain("/watch"); }); diff --git a/packages/vite-plugin-cloudflare/src/__tests__/shortcuts.spec.ts b/packages/vite-plugin-cloudflare/src/__tests__/shortcuts.spec.ts index 55480575eee..63a50f269ea 100644 --- a/packages/vite-plugin-cloudflare/src/__tests__/shortcuts.spec.ts +++ b/packages/vite-plugin-cloudflare/src/__tests__/shortcuts.spec.ts @@ -264,7 +264,9 @@ describe.skipIf(!satisfiesMinimumViteVersion("7.2.7"))("shortcuts", () => { await explorerShortcut?.action?.(mockServer); expect(mockOpen).toHaveBeenCalledWith( - expect.stringMatching(/^http:\/\/localhost:\d+\/cdn-cgi\/explorer$/) + expect.stringMatching( + /^http:\/\/localhost:\d+\/cdn-cgi\/local\/explorer$/ + ) ); }); diff --git a/packages/vite-plugin-cloudflare/src/plugins/preview.ts b/packages/vite-plugin-cloudflare/src/plugins/preview.ts index 1617b295de7..4ef4e1d7709 100644 --- a/packages/vite-plugin-cloudflare/src/plugins/preview.ts +++ b/packages/vite-plugin-cloudflare/src/plugins/preview.ts @@ -5,13 +5,14 @@ import { } from "@cloudflare/containers-shared"; import { cleanupContainers } from "@cloudflare/containers-shared/src/utils"; import { UserError } from "@cloudflare/workers-utils"; -import { buildPublicUrl } from "miniflare"; +import { buildPublicUrl, Request as MiniflareRequest } from "miniflare"; import colors from "picocolors"; import { getDockerPath } from "../containers"; import { assertIsPreview } from "../context"; import { getPreviewMiniflareOptions } from "../miniflare-options"; import { createPlugin, createRequestHandler } from "../utils"; import { handleWebSocket } from "../websockets"; +import { rewriteLegacyMiniflarePath } from "./trigger-handlers"; let exitCallback = () => {}; @@ -121,6 +122,12 @@ export const previewPlugin = createPlugin("preview", (ctx) => { // In preview mode we put our middleware at the front of the chain so that all assets are handled in Miniflare vitePreviewServer.middlewares.use( createRequestHandler((request) => { + const url = new URL(request.url); + const rewritten = rewriteLegacyMiniflarePath(url.pathname); + if (rewritten !== url.pathname) { + url.pathname = rewritten; + request = new MiniflareRequest(url, request); + } return ctx.miniflare.dispatchFetch(request, { redirect: "manual" }); }) ); diff --git a/packages/vite-plugin-cloudflare/src/plugins/trigger-handlers.ts b/packages/vite-plugin-cloudflare/src/plugins/trigger-handlers.ts index 961f1c28cdf..ae1bbb44bd5 100644 --- a/packages/vite-plugin-cloudflare/src/plugins/trigger-handlers.ts +++ b/packages/vite-plugin-cloudflare/src/plugins/trigger-handlers.ts @@ -1,8 +1,31 @@ -import { CoreHeaders } from "miniflare"; +import { CoreHeaders, Request as MiniflareRequest } from "miniflare"; import { createPlugin, createRequestHandler } from "../utils"; +// Miniflare v5 moved its internal endpoints under `/cdn-cgi/local/` (and +// `/__cf_local/` for endpoints that must remain reachable over tunnels). These +// map the pre-v5 paths onto their current equivalents. This must stay in sync +// with `rewriteLegacyMiniflarePath()` in Wrangler's ProxyWorker. +const LEGACY_PATH_REWRITES: readonly [string, string][] = [ + ["/cdn-cgi/handler", "/cdn-cgi/local"], + ["/cdn-cgi/mf/scheduled", "/cdn-cgi/local/scheduled"], + ["/cdn-cgi/mf/stream", "/__cf_local/stream"], + ["/cdn-cgi/mf/imagedelivery", "/__cf_local/imagedelivery"], + ["/cdn-cgi/explorer", "/cdn-cgi/local/explorer"], +]; + +export function rewriteLegacyMiniflarePath(pathname: string): string { + for (const [oldPrefix, newPrefix] of LEGACY_PATH_REWRITES) { + if (pathname === oldPrefix || pathname.startsWith(`${oldPrefix}/`)) { + return newPrefix + pathname.slice(oldPrefix.length); + } + } + return pathname; +} + /** - * Plugin to forward `/cdn-cgi/handler/*` routes to trigger handlers in development + * Plugin to forward trigger handler routes (scheduled, email) and other + * internal Miniflare endpoints to Miniflare in development, including + * backwards-compatible rewrites for pre-v5 paths. */ export const triggerHandlersPlugin = createPlugin("trigger-handlers", (ctx) => { return { @@ -15,14 +38,32 @@ export const triggerHandlersPlugin = createPlugin("trigger-handlers", (ctx) => { } const entryWorkerName = entryWorkerConfig.name; - const requestHandler = createRequestHandler((request) => { + + function dispatch(request: MiniflareRequest) { request.headers.set(CoreHeaders.ROUTE_OVERRIDE, entryWorkerName); return ctx.miniflare.dispatchFetch(request, { redirect: "manual", }); - }); + } + + // Canonical paths: forward directly to Miniflare. + viteDevServer.middlewares.use( + "/cdn-cgi/local/", + createRequestHandler((request) => dispatch(request)) + ); - viteDevServer.middlewares.use("/cdn-cgi/handler/", requestHandler); + // Backwards compatibility: rewrite legacy paths onto their canonical + // equivalents before dispatching. + for (const [oldPrefix, newPrefix] of LEGACY_PATH_REWRITES) { + viteDevServer.middlewares.use( + oldPrefix, + createRequestHandler((request) => { + const url = new URL(request.url); + url.pathname = newPrefix + url.pathname.slice(oldPrefix.length); + return dispatch(new MiniflareRequest(url, request)); + }) + ); + } }, }; }); diff --git a/packages/workers-utils/src/environment-variables/factory.ts b/packages/workers-utils/src/environment-variables/factory.ts index 03ee9f4566a..bf0a699dc48 100644 --- a/packages/workers-utils/src/environment-variables/factory.ts +++ b/packages/workers-utils/src/environment-variables/factory.ts @@ -114,7 +114,7 @@ type VariableNames = // ## Experimental Feature Flags - /** Enable the local explorer UI at /cdn-cgi/explorer (experimental, default: false). */ + /** Enable the local explorer UI at /cdn-cgi/local/explorer (experimental, default: false). */ | "X_LOCAL_EXPLORER" /** Open the browser in headful (visible) mode when using the Browser Run API in local dev (default: false). */ | "X_BROWSER_HEADFUL" diff --git a/packages/workers-utils/src/environment-variables/misc-variables.ts b/packages/workers-utils/src/environment-variables/misc-variables.ts index 5b0d551a490..4b48913f80d 100644 --- a/packages/workers-utils/src/environment-variables/misc-variables.ts +++ b/packages/workers-utils/src/environment-variables/misc-variables.ts @@ -345,7 +345,7 @@ export const getOpenNextDeployFromEnv = getEnvironmentVariableFactory({ }); /** - * `X_LOCAL_EXPLORER` enables the local explorer UI at /cdn-cgi/explorer. + * `X_LOCAL_EXPLORER` enables the local explorer UI at /cdn-cgi/local/explorer. */ export const getLocalExplorerEnabledFromEnv = getBooleanEnvironmentVariableFactory({ diff --git a/packages/wrangler/e2e/createTestHarness.test.ts b/packages/wrangler/e2e/createTestHarness.test.ts index d103a41b89b..ab40daaa608 100644 --- a/packages/wrangler/e2e/createTestHarness.test.ts +++ b/packages/wrangler/e2e/createTestHarness.test.ts @@ -1795,8 +1795,8 @@ describe("createTestHarness", () => { [server] startup - completed [server] fetch - GET / - started [server] fetch - GET / - 200 - [server] [scheduled-worker] scheduled - GET /cdn-cgi/handler/scheduled?format=json&cron=*+*+*+*+*&time=1700000100000 - started - [server] [scheduled-worker] scheduled - GET /cdn-cgi/handler/scheduled?format=json&cron=*+*+*+*+*&time=1700000100000 - 200 + [server] [scheduled-worker] scheduled - GET /cdn-cgi/local/scheduled?format=json&cron=*+*+*+*+*&time=1700000100000 - started + [server] [scheduled-worker] scheduled - GET /cdn-cgi/local/scheduled?format=json&cron=*+*+*+*+*&time=1700000100000 - 200 [server] fetch - GET / - started [server] fetch - GET / - 200" `); diff --git a/packages/wrangler/e2e/dev.test.ts b/packages/wrangler/e2e/dev.test.ts index 35b1477a3b6..e107b0e8230 100644 --- a/packages/wrangler/e2e/dev.test.ts +++ b/packages/wrangler/e2e/dev.test.ts @@ -300,7 +300,7 @@ describe.each([ "Scheduled Workers are not automatically triggered" ); expect(worker.currentOutput).toContain( - `curl "http://${hostname}:${port}/cdn-cgi/handler/scheduled"` + `curl "http://${hostname}:${port}/cdn-cgi/local/scheduled"` ); expect(worker.currentOutput).not.toContain("undefined"); }); @@ -2395,7 +2395,7 @@ This is a random email body. const { url } = await worker.waitForReady(); const response = await fetch( - `${url}/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com`, + `${url}/cdn-cgi/local/email?from=someone@example.com&to=someone-else@example.com`, { body: dedent` From: someone @@ -2442,15 +2442,20 @@ This is a random email body. `); }); - it("should print reject with reason", async ({ expect }) => { - const helper = new WranglerE2ETestHelper(); - await helper.seed({ - "wrangler.toml": dedent` + // The canonical path is `/cdn-cgi/local/email`; `/cdn-cgi/handler/email` is + // the legacy path kept working via a rewrite in the dev proxy. + describe.each(["/cdn-cgi/local/email", "/cdn-cgi/handler/email"])( + "%s", + (path) => { + it("should print reject with reason", async ({ expect }) => { + const helper = new WranglerE2ETestHelper(); + await helper.seed({ + "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2025-03-17" `, - "src/index.ts": dedent` + "src/index.ts": dedent` import { EmailMessage } from "cloudflare:email"; export default { @@ -2458,16 +2463,16 @@ This is a random email body. await emailMessage.setReject('I dont like this email') } }`, - }); + }); - const worker = helper.runLongLived("wrangler dev"); + const worker = helper.runLongLived("wrangler dev"); - const { url } = await worker.waitForReady(); + const { url } = await worker.waitForReady(); - const response = await fetch( - `${url}/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com`, - { - body: `From: someone + const response = await fetch( + `${url}${path}?from=someone@example.com&to=someone-else@example.com`, + { + body: `From: someone To: someone else MIME-Version: 1.0 Message-ID: @@ -2475,16 +2480,18 @@ Content-Type: text/plain This is a random email body. `, - method: "POST", - } - ); + method: "POST", + } + ); - expect(await response.text()).toMatchInlineSnapshot( - `"Worker rejected email with the following reason: I dont like this email"` - ); + expect(await response.text()).toMatchInlineSnapshot( + `"Worker rejected email with the following reason: I dont like this email"` + ); - expect(response.status).toBe(400); - }); + expect(response.status).toBe(400); + }); + } + ); it("should print forward email", async ({ expect }) => { const helper = new WranglerE2ETestHelper(); @@ -2509,7 +2516,7 @@ This is a random email body. const { url } = await worker.waitForReady(); const response = await fetch( - `${url}/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com`, + `${url}/cdn-cgi/local/email?from=someone@example.com&to=someone-else@example.com`, { body: `From: someone To: someone else diff --git a/packages/wrangler/e2e/multiworker-dev.test.ts b/packages/wrangler/e2e/multiworker-dev.test.ts index 4a44c137749..e3acb5aee1a 100644 --- a/packages/wrangler/e2e/multiworker-dev.test.ts +++ b/packages/wrangler/e2e/multiworker-dev.test.ts @@ -643,7 +643,7 @@ describe("multiworker", () => { "Scheduled Workers are not automatically triggered" ); expect(worker.currentOutput).toContain( - `curl "http://${hostname}:${port}/cdn-cgi/handler/scheduled"` + `curl "http://${hostname}:${port}/cdn-cgi/local/scheduled"` ); expect(worker.currentOutput).not.toContain("undefined"); }); diff --git a/packages/wrangler/src/__tests__/workflows.test.ts b/packages/wrangler/src/__tests__/workflows.test.ts index 148dbc75c58..fa5b27b085f 100644 --- a/packages/wrangler/src/__tests__/workflows.test.ts +++ b/packages/wrangler/src/__tests__/workflows.test.ts @@ -1364,12 +1364,12 @@ describe("wrangler workflows", () => { }); // ========================================================================= - // Local commands (--local) — hitting /cdn-cgi/explorer/api/workflows/... + // Local commands (--local) — hitting /cdn-cgi/local/explorer/api/workflows/... // ========================================================================= describe("local", () => { const LOCAL_PORT = 8787; - const LOCAL_BASE = `http://localhost:${LOCAL_PORT}/cdn-cgi/explorer/api`; + const LOCAL_BASE = `http://localhost:${LOCAL_PORT}/cdn-cgi/local/explorer/api`; describe("workflows list --local", () => { it("should list workflows from local dev session", async ({ expect }) => { diff --git a/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts b/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts index 9997e287cb1..1ca8cae9c9c 100644 --- a/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts +++ b/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts @@ -194,7 +194,6 @@ export async function convertToConfigBundle( inspectorHost: event.config.dev.inspector?.hostname, }), localPersistencePath: event.config.dev.persist, - liveReload: event.config.dev?.liveReload ?? false, crons, routes: event.config.dev.routeRequestsByRoutes ? routes : undefined, queueConsumers, @@ -408,8 +407,6 @@ export class LocalRuntimeController extends RuntimeController { }); } ); - options.liveReload = false; // TODO: set in buildMiniflareOptions once old code path is removed - options.handleUncaughtError = this.dispatchRuntimeError; // Bail out if a newer bundle arrived while we were building // miniflare options — avoid a redundant local server reload. diff --git a/packages/wrangler/src/api/startDevWorker/ProxyController.ts b/packages/wrangler/src/api/startDevWorker/ProxyController.ts index 991c6b1657f..1bfcc3a449f 100644 --- a/packages/wrangler/src/api/startDevWorker/ProxyController.ts +++ b/packages/wrangler/src/api/startDevWorker/ProxyController.ts @@ -133,7 +133,6 @@ export class ProxyController extends Controller { this.localServerReady.promise ), handleStructuredLogs, - liveReload: false, }; if (this.inspectorEnabled) { diff --git a/packages/wrangler/src/api/test-harness.ts b/packages/wrangler/src/api/test-harness.ts index 5e1c0f2cd22..bccc2721218 100644 --- a/packages/wrangler/src/api/test-harness.ts +++ b/packages/wrangler/src/api/test-harness.ts @@ -989,7 +989,7 @@ export function createTestHarness(options?: TestHarnessOptions): TestHarness { const response = await dispatchFetch( miniflare, - `/cdn-cgi/handler/scheduled?${searchParams.toString()}`, + `/cdn-cgi/local/scheduled?${searchParams.toString()}`, undefined, workerName, "scheduled" diff --git a/packages/wrangler/src/dev/miniflare/index.ts b/packages/wrangler/src/dev/miniflare/index.ts index ff4be0e2aca..50bf3e46cd5 100644 --- a/packages/wrangler/src/dev/miniflare/index.ts +++ b/packages/wrangler/src/dev/miniflare/index.ts @@ -87,7 +87,6 @@ export interface ConfigBundle { inspectorPort: number | undefined; inspectorHost: string | undefined; localPersistencePath: string | false; - liveReload: boolean; crons: Config["triggers"]["crons"]; routes: string[] | undefined; queueConsumers: Config["queues"]["consumers"]; @@ -1169,7 +1168,6 @@ export async function buildMiniflareOptions( publicUrl: config.publicUrl, inspectorPort: config.inspect ? config.inspectorPort : undefined, inspectorHost: config.inspect ? config.inspectorHost : undefined, - liveReload: config.liveReload, upstream, unsafeDevRegistryPath: config.devRegistry, unsafeHandleDevRegistryUpdate: onDevRegistryUpdate, diff --git a/packages/wrangler/src/dev/start-dev.ts b/packages/wrangler/src/dev/start-dev.ts index f7a27d61877..c77804fe2f3 100644 --- a/packages/wrangler/src/dev/start-dev.ts +++ b/packages/wrangler/src/dev/start-dev.ts @@ -362,7 +362,7 @@ function maybePrintScheduledWorkerWarning( logger.once.warn( `Scheduled Workers are not automatically triggered during local development.\n` + `To manually trigger a scheduled event, run:\n` + - ` curl "http://${host}:${port}/cdn-cgi/handler/scheduled"\n` + + ` curl "http://${host}:${port}/cdn-cgi/local/scheduled"\n` + `For more details, see https://developers.cloudflare.com/workers/configuration/cron-triggers/#test-cron-triggers-locally` ); } diff --git a/packages/wrangler/src/workflows/local.ts b/packages/wrangler/src/workflows/local.ts index 505872aedd4..bb73fdce884 100644 --- a/packages/wrangler/src/workflows/local.ts +++ b/packages/wrangler/src/workflows/local.ts @@ -6,7 +6,7 @@ import type { WorkflowInstanceRestartFrom, } from "./types"; -const LOCAL_EXPLORER_BASE_PATH = "/cdn-cgi/explorer/api"; +const LOCAL_EXPLORER_BASE_PATH = "/cdn-cgi/local/explorer/api"; const DEFAULT_LOCAL_PORT = 8787; /** diff --git a/packages/wrangler/templates/new-worker-scheduled.js b/packages/wrangler/templates/new-worker-scheduled.js index a56d30b72af..c95c7b1c10e 100644 --- a/packages/wrangler/templates/new-worker-scheduled.js +++ b/packages/wrangler/templates/new-worker-scheduled.js @@ -2,7 +2,7 @@ * Welcome to Cloudflare Workers! This is your first scheduled worker. * * - Run `wrangler dev` in your terminal to start a development server - * - Run `curl "http://localhost:8787/cdn-cgi/handler/scheduled"` to trigger the scheduled event + * - Run `curl "http://localhost:8787/cdn-cgi/local/scheduled"` to trigger the scheduled event * - Go back to the console to see what your worker has logged * - Update the Cron trigger in wrangler.toml (see https://developers.cloudflare.com/workers/configuration/cron-triggers/) * - Run `wrangler publish --name my-worker` to publish your worker diff --git a/packages/wrangler/templates/new-worker-scheduled.ts b/packages/wrangler/templates/new-worker-scheduled.ts index f7f5992dfc8..67fef0d66c5 100644 --- a/packages/wrangler/templates/new-worker-scheduled.ts +++ b/packages/wrangler/templates/new-worker-scheduled.ts @@ -2,7 +2,7 @@ * Welcome to Cloudflare Workers! This is your first scheduled worker. * * - Run `wrangler dev` in your terminal to start a development server - * - Run `curl "http://localhost:8787/cdn-cgi/handler/scheduled"` to trigger the scheduled event + * - Run `curl "http://localhost:8787/cdn-cgi/local/scheduled"` to trigger the scheduled event * - Go back to the console to see what your worker has logged * - Update the Cron trigger in wrangler.toml (see https://developers.cloudflare.com/workers/configuration/cron-triggers/) * - Run `wrangler deploy --name my-worker` to deploy your worker diff --git a/packages/wrangler/templates/startDevWorker/ProxyWorker.ts b/packages/wrangler/templates/startDevWorker/ProxyWorker.ts index fc28268c008..32c92f6ba4b 100644 --- a/packages/wrangler/templates/startDevWorker/ProxyWorker.ts +++ b/packages/wrangler/templates/startDevWorker/ProxyWorker.ts @@ -131,6 +131,13 @@ export class ProxyWorker implements DurableObject { request.url ); + // rewrite requests to old miniflare paths + // because wrangler cannot have a breaking change + userWorkerUrl.pathname = rewriteLegacyMiniflarePath( + userWorkerUrl.pathname + ); + innerUrl.pathname = rewriteLegacyMiniflarePath(innerUrl.pathname); + // Preserve client `Accept-Encoding`, rather than using Worker's default // of `Accept-Encoding: br, gzip` const encoding = request.cf?.clientAcceptEncoding; @@ -236,6 +243,26 @@ export class ProxyWorker implements DurableObject { function isRequestFromProxyController(req: Request, env: Env): boolean { return req.headers.get("Authorization") === env.PROXY_CONTROLLER_AUTH_SECRET; } + +// Miniflare v5 moved its internal endpoints under `/cdn-cgi/local/` (and +// `/__cf_local/` for endpoints that must remain reachable over tunnels). These +// map the pre-v5 paths onto their current equivalents. +const LEGACY_PATH_REWRITES: readonly [string, string][] = [ + ["/cdn-cgi/handler", "/cdn-cgi/local"], + ["/cdn-cgi/mf/scheduled", "/cdn-cgi/local/scheduled"], + ["/cdn-cgi/mf/stream", "/__cf_local/stream"], + ["/cdn-cgi/mf/imagedelivery", "/__cf_local/imagedelivery"], + ["/cdn-cgi/explorer", "/cdn-cgi/local/explorer"], +]; + +function rewriteLegacyMiniflarePath(pathname: string): string { + for (const [oldPrefix, newPrefix] of LEGACY_PATH_REWRITES) { + if (pathname === oldPrefix || pathname.startsWith(`${oldPrefix}/`)) { + return newPrefix + pathname.slice(oldPrefix.length); + } + } + return pathname; +} function isHtmlResponse(res: Response): boolean { return res.headers.get("content-type")?.startsWith("text/html") ?? false; } From 45efc7c53afb6129a2f3e8c2d63b137d7df44797 Mon Sep 17 00:00:00 2001 From: emily-shen <69125074+emily-shen@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:26:21 +0100 Subject: [PATCH 3/6] [miniflare v5] update miniflare and co to zod v4 (#14716) --- .changeset/zod-v4-miniflare.md | 7 + packages/miniflare/package.json | 2 +- packages/miniflare/src/index.ts | 11 +- .../src/plugins/agent-memory/index.ts | 2 +- .../miniflare/src/plugins/ai-search/index.ts | 4 +- .../src/plugins/analytics-engine/index.ts | 1 + .../miniflare/src/plugins/artifacts/index.ts | 2 +- packages/miniflare/src/plugins/core/index.ts | 48 +- .../miniflare/src/plugins/core/services.ts | 35 +- packages/miniflare/src/plugins/d1/index.ts | 1 + .../src/plugins/dispatch-namespace/index.ts | 1 + packages/miniflare/src/plugins/do/index.ts | 11 +- .../miniflare/src/plugins/flagship/index.ts | 2 +- .../src/plugins/hello-world/index.ts | 1 + .../miniflare/src/plugins/hyperdrive/index.ts | 14 +- packages/miniflare/src/plugins/kv/index.ts | 1 + packages/miniflare/src/plugins/mtls/index.ts | 2 +- .../miniflare/src/plugins/pipelines/index.ts | 1 + .../miniflare/src/plugins/queues/index.ts | 20 +- packages/miniflare/src/plugins/r2/index.ts | 1 + .../miniflare/src/plugins/ratelimit/index.ts | 4 +- .../src/plugins/secret-store/index.ts | 1 + .../miniflare/src/plugins/shared/index.ts | 6 +- .../miniflare/src/plugins/vectorize/index.ts | 2 +- .../src/plugins/vpc-networks/index.ts | 2 +- .../src/plugins/vpc-services/index.ts | 2 +- .../miniflare/src/plugins/websearch/index.ts | 2 +- .../src/plugins/worker-loader/index.ts | 2 +- .../miniflare/src/plugins/workflows/index.ts | 1 + packages/miniflare/src/shared/colour.ts | 10 - packages/miniflare/src/shared/index.ts | 1 - packages/miniflare/src/shared/types.ts | 27 +- .../src/workers/local-explorer/common.ts | 26 +- .../local-explorer/generated/zod.gen.ts | 69 +-- .../miniflare/src/workers/queues/schemas.ts | 10 +- .../src/workers/r2/schemas.worker.ts | 24 +- packages/miniflare/test/index.spec.ts | 6 +- .../test/plugins/local-explorer/helpers.ts | 6 +- .../test/plugins/local-explorer/index.spec.ts | 8 +- .../test/plugins/queues/index.spec.ts | 2 +- .../test/snapshots/zod-format.spec.ts.md | 307 ------------ .../test/snapshots/zod-format.spec.ts.snap | Bin 1517 -> 0 bytes packages/miniflare/test/zod-format.spec.ts | 458 ------------------ .../__tests__/email-triggers.spec.ts | 6 +- packages/vitest-pool-workers/package.json | 2 +- .../vitest-pool-workers/src/pool/config.ts | 18 +- .../test/validation.test.ts | 6 +- packages/workers-shared/package.json | 2 +- packages/workers-shared/utils/types.ts | 11 +- packages/workers-utils/package.json | 4 +- packages/workers-utils/src/index.ts | 2 + .../src/zod-format.ts | 136 ++++-- .../workers-utils/tests/zod-format.test.ts | 458 ++++++++++++++++++ .../startDevWorker/LocalRuntimeController.ts | 1 + pnpm-lock.yaml | 419 +++++++++------- tools/deployments/validate-catalog-usage.ts | 7 +- 56 files changed, 1032 insertions(+), 1183 deletions(-) create mode 100644 .changeset/zod-v4-miniflare.md delete mode 100644 packages/miniflare/src/shared/colour.ts delete mode 100644 packages/miniflare/test/snapshots/zod-format.spec.ts.md delete mode 100644 packages/miniflare/test/snapshots/zod-format.spec.ts.snap delete mode 100644 packages/miniflare/test/zod-format.spec.ts rename packages/{miniflare => workers-utils}/src/zod-format.ts (83%) create mode 100644 packages/workers-utils/tests/zod-format.test.ts diff --git a/.changeset/zod-v4-miniflare.md b/.changeset/zod-v4-miniflare.md new file mode 100644 index 00000000000..e700a92fe41 --- /dev/null +++ b/.changeset/zod-v4-miniflare.md @@ -0,0 +1,7 @@ +--- +"miniflare": major +--- + +Upgrade to zod v4 + +Miniflare's exported schemas now use zod v4. diff --git a/packages/miniflare/package.json b/packages/miniflare/package.json index e9968ff0b33..598486a5d08 100644 --- a/packages/miniflare/package.json +++ b/packages/miniflare/package.json @@ -100,7 +100,7 @@ "typescript": "catalog:default", "vitest": "catalog:default", "which": "^2.0.2", - "zod": "^3.25.76" + "zod": "catalog:default" }, "engines": { "node": ">=22.0.0" diff --git a/packages/miniflare/src/index.ts b/packages/miniflare/src/index.ts index 4216b7d9902..5d977d7799f 100644 --- a/packages/miniflare/src/index.ts +++ b/packages/miniflare/src/index.ts @@ -11,6 +11,7 @@ import util from "node:util"; import zlib from "node:zlib"; import { checkMacOSVersion } from "@cloudflare/cli-shared-helpers"; import { removeDir, removeDirSync } from "@cloudflare/workers-utils"; +import { formatZodError } from "@cloudflare/workers-utils"; import { $ as colors$, bold, dim, green, yellow } from "kleur/colors"; import stoppable from "stoppable"; import { getGlobalDispatcher, Pool } from "undici"; @@ -117,7 +118,6 @@ import { SiteBindings, } from "./workers"; import { ADMIN_API } from "./workers/secrets-store/constants"; -import { formatZodError } from "./zod-format"; import type { DispatchFetch, RequestInit } from "./http"; import type { DurableObjectClassNames, @@ -986,7 +986,7 @@ export class Miniflare { * externalPlugins is a list of external plugins that have been loaded * after being referenced by an unsafe binding */ - #externalPlugins: Map> = new Map(); + #externalPlugins: Map> = new Map(); // key is the browser session ID, value identifies the launched browser #browserProcesses: Map< @@ -2074,6 +2074,8 @@ export class Miniflare { for (const [key, plugin] of this.#mergedPluginEntries) { const pluginBindings = await plugin.getBindings( + // @ts-expect-error dynamic plugin dispatch: external plugins return + // a different type than internal plugin options this.#getWorkerOptsForPlugin(key, workerOpts), i ); @@ -2153,7 +2155,7 @@ export class Miniflare { this.publicUrl = sharedOpts.core.publicUrl; const pluginServicesOptionsBase: Omit< - PluginServicesOptions, + PluginServicesOptions, "options" | "sharedOptions" > = { log: this.#log, @@ -2990,6 +2992,8 @@ export class Miniflare { // Populate bindings from each plugin for (const [key, plugin] of this.#mergedPluginEntries) { const pluginBindings = await plugin.getNodeBindings( + // @ts-expect-error dynamic plugin dispatch: external plugins return + // a different type than internal plugin options this.#getWorkerOptsForPlugin(key, workerOpts) ); for (const [name, binding] of Object.entries(pluginBindings)) { @@ -3456,7 +3460,6 @@ export * from "./runtime"; export * from "./shared"; export * from "./workers"; export * from "./merge"; -export * from "./zod-format"; export type { DurableObjectIdentifier, DurableObjectEvictionOptions, diff --git a/packages/miniflare/src/plugins/agent-memory/index.ts b/packages/miniflare/src/plugins/agent-memory/index.ts index 5a04bab0210..be60145a219 100644 --- a/packages/miniflare/src/plugins/agent-memory/index.ts +++ b/packages/miniflare/src/plugins/agent-memory/index.ts @@ -14,7 +14,7 @@ const AgentMemoryEntrySchema = z.object({ }); export const AgentMemoryOptionsSchema = z.object({ - agentMemory: z.record(AgentMemoryEntrySchema).optional(), + agentMemory: z.record(z.string(), AgentMemoryEntrySchema).optional(), }); export const AGENT_MEMORY_PLUGIN_NAME = "agent-memory"; diff --git a/packages/miniflare/src/plugins/ai-search/index.ts b/packages/miniflare/src/plugins/ai-search/index.ts index 0c40ab13529..7779c8c1a3c 100644 --- a/packages/miniflare/src/plugins/ai-search/index.ts +++ b/packages/miniflare/src/plugins/ai-search/index.ts @@ -15,8 +15,8 @@ const AISearchEntrySchema = z.object({ }); export const AISearchOptionsSchema = z.object({ - aiSearchNamespaces: z.record(AISearchEntrySchema).optional(), - aiSearchInstances: z.record(AISearchEntrySchema).optional(), + aiSearchNamespaces: z.record(z.string(), AISearchEntrySchema).optional(), + aiSearchInstances: z.record(z.string(), AISearchEntrySchema).optional(), }); export const AI_SEARCH_PLUGIN_NAME = "ai-search"; diff --git a/packages/miniflare/src/plugins/analytics-engine/index.ts b/packages/miniflare/src/plugins/analytics-engine/index.ts index 61b614ecc3f..98e48db4824 100644 --- a/packages/miniflare/src/plugins/analytics-engine/index.ts +++ b/packages/miniflare/src/plugins/analytics-engine/index.ts @@ -5,6 +5,7 @@ import type { Worker_Binding } from "../../runtime"; import type { Plugin } from "../shared"; const AnalyticsEngineSchema = z.record( + z.string(), z.object({ dataset: z.string(), }) diff --git a/packages/miniflare/src/plugins/artifacts/index.ts b/packages/miniflare/src/plugins/artifacts/index.ts index 79231159fa3..2ac00d49fb9 100644 --- a/packages/miniflare/src/plugins/artifacts/index.ts +++ b/packages/miniflare/src/plugins/artifacts/index.ts @@ -14,7 +14,7 @@ const ArtifactsSchema = z.object({ }); export const ArtifactsOptionsSchema = z.object({ - artifacts: z.record(ArtifactsSchema).optional(), + artifacts: z.record(z.string(), ArtifactsSchema).optional(), }); export const ARTIFACTS_PLUGIN_NAME = "artifacts"; diff --git a/packages/miniflare/src/plugins/core/index.ts b/packages/miniflare/src/plugins/core/index.ts index 7f1d87efeb9..6cdae27f553 100644 --- a/packages/miniflare/src/plugins/core/index.ts +++ b/packages/miniflare/src/plugins/core/index.ts @@ -130,18 +130,18 @@ export function createFetchMock() { const WrappedBindingSchema = z.object({ scriptName: z.string(), entrypoint: z.string().optional(), - bindings: z.record(JsonSchema).optional(), + bindings: z.record(z.string(), JsonSchema).optional(), }); // 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(), - serviceName: z.ostring(), - entrypoint: z.ostring(), - proxy: z.oboolean(), + host: z.string().optional(), + port: z.number().optional(), + serviceName: z.string().optional(), + entrypoint: z.string().optional(), + proxy: z.boolean().optional(), }); export const ExternalPluginSpecifier = z.object({ @@ -162,17 +162,17 @@ const CoreOptionsSchemaInput = z.intersection( routes: z.string().array().optional(), - bindings: z.record(JsonSchema).optional(), + bindings: z.record(z.string(), JsonSchema).optional(), wasmBindings: z - .record(z.union([PathSchema, z.instanceof(Uint8Array)])) + .record(z.string(), z.union([PathSchema, z.instanceof(Uint8Array)])) .optional(), - textBlobBindings: z.record(PathSchema).optional(), + textBlobBindings: z.record(z.string(), PathSchema).optional(), dataBlobBindings: z - .record(z.union([PathSchema, z.instanceof(Uint8Array)])) + .record(z.string(), z.union([PathSchema, z.instanceof(Uint8Array)])) .optional(), - serviceBindings: z.record(ServiceDesignatorSchema).optional(), + serviceBindings: z.record(z.string(), ServiceDesignatorSchema).optional(), wrappedBindings: z - .record(z.union([z.string(), WrappedBindingSchema])) + .record(z.string(), z.union([z.string(), WrappedBindingSchema])) .optional(), outboundService: ServiceDesignatorSchema.optional(), @@ -234,7 +234,7 @@ const CoreOptionsSchemaInput = z.intersection( name: z.string(), type: z.string(), plugin: ExternalPluginSpecifier, - options: z.record(JsonSchema), + options: z.record(z.string(), JsonSchema), }) ) .optional(), @@ -287,12 +287,16 @@ export const CoreSharedOptionsSchema = z log: z.instanceof(Log).optional(), handleRuntimeStdio: z - .function(z.tuple([z.instanceof(Readable), z.instanceof(Readable)])) + .function({ + input: [z.instanceof(Readable), z.instanceof(Readable)], + }) .optional(), handleStructuredLogs: z - .function(z.tuple([WorkerdStructuredLogSchema])) - .returns(z.void()) + .function({ + input: [WorkerdStructuredLogSchema], + output: z.void(), + }) .optional(), // Deliberately not `z.function()`: parsing that schema replaces the @@ -310,13 +314,17 @@ export const CoreSharedOptionsSchema = z upstream: z.string().optional(), // TODO: add back validation of cf object - cf: z.union([z.boolean(), z.string(), z.record(z.any())]).optional(), + cf: z + .union([z.boolean(), z.string(), z.record(z.string(), z.any())]) + .optional(), // Enable auto service / durable objects discovery with the dev registry unsafeDevRegistryPath: z.string().optional(), // Called when external workers this instance depends on are updated in the dev registry unsafeHandleDevRegistryUpdate: z - .function(z.tuple([z.custom()])) + .function({ + input: [z.custom()], + }) .optional(), // This is a shared secret between a proxy server and miniflare that can be // passed in a header to prove that the request came from the proxy and not @@ -331,7 +339,7 @@ export const CoreSharedOptionsSchema = z // Merged on top of `process.env` and Miniflare's own defaults // (e.g. `TZ=UTC`, `FORCE_COLOR`), so callers can override those defaults // (for example, to test timezone-dependent behaviour). - unsafeRuntimeEnv: z.record(z.string()).optional(), + unsafeRuntimeEnv: z.record(z.string(), z.string()).optional(), // Enable the local explorer at /cdn-cgi/local/explorer unsafeLocalExplorer: z.boolean().optional(), // Enable RPC-based Durable Object introspection APIs @@ -365,7 +373,7 @@ export const CoreSharedOptionsSchema = z // (e.g. the Wrangler proxy URL or Vite dev server URL). Used by // plugins like Stream to generate preview URLs that outlive runtime // restarts. If not set, plugins fall back to the runtime entry URL. - publicUrl: z.string().url().optional(), + publicUrl: z.url().optional(), }) .refine( ({ structuredWorkerdLogs, handleStructuredLogs }) => { diff --git a/packages/miniflare/src/plugins/core/services.ts b/packages/miniflare/src/plugins/core/services.ts index 3fa0fa14aa0..5fd9ee8069c 100644 --- a/packages/miniflare/src/plugins/core/services.ts +++ b/packages/miniflare/src/plugins/core/services.ts @@ -18,13 +18,13 @@ export const kCurrentWorker = Symbol.for("miniflare.kCurrentWorker"); export const HttpOptionsHeaderSchema = z.object({ name: z.string(), // name should be required - value: z.ostring(), // If omitted, the header will be removed + value: z.string().optional(), // If omitted, the header will be removed }); const HttpOptionsSchema = z .object({ - style: z.nativeEnum(HttpOptions_Style).optional(), - forwardedProtoHeader: z.ostring(), - cfBlobHeader: z.ostring(), + style: z.enum(HttpOptions_Style).optional(), + forwardedProtoHeader: z.string().optional(), + cfBlobHeader: z.string().optional(), injectRequestHeaders: HttpOptionsHeaderSchema.array().optional(), injectResponseHeaders: HttpOptionsHeaderSchema.array().optional(), }) @@ -34,17 +34,17 @@ const HttpOptionsSchema = z })); const TlsOptionsKeypairSchema = z.object({ - privateKey: z.ostring(), - certificateChain: z.ostring(), + privateKey: z.string().optional(), + certificateChain: z.string().optional(), }); const TlsOptionsSchema = z.object({ keypair: TlsOptionsKeypairSchema.optional(), - requireClientCerts: z.oboolean(), - trustBrowserCas: z.oboolean(), + requireClientCerts: z.boolean().optional(), + trustBrowserCas: z.boolean().optional(), trustedCertificates: z.string().array().optional(), - minVersion: z.nativeEnum(TlsOptions_Version).optional(), - cipherList: z.ostring(), + minVersion: z.enum(TlsOptions_Version).optional(), + cipherList: z.string().optional(), }); const NetworkSchema = z.object({ @@ -62,7 +62,7 @@ export const ExternalServerSchema = z.intersection( z.object({ options: HttpOptionsSchema.optional(), tlsOptions: TlsOptionsSchema.optional(), - certificateHost: z.ostring(), + certificateHost: z.string().optional(), }) ), }), @@ -76,7 +76,7 @@ export const ExternalServerSchema = z.intersection( const DiskDirectorySchema = z.object({ path: z.string(), // path should be required - writable: z.oboolean(), + writable: z.boolean().optional(), }); const CustomNodeServiceSchema = z.custom< @@ -93,11 +93,14 @@ export const CustomFetchServiceSchema = z.custom< export const ServiceDesignatorSchema = z.union([ z.string(), - z.literal(kCurrentWorker), + z.custom((v) => v === kCurrentWorker), z.object({ - name: z.union([z.string(), z.literal(kCurrentWorker)]), - entrypoint: z.ostring(), - props: z.record(z.unknown()).optional(), + name: z.union([ + z.string(), + z.custom((v) => v === kCurrentWorker), + ]), + entrypoint: z.string().optional(), + props: z.record(z.string(), z.unknown()).optional(), remoteProxyConnectionString: z .custom() .optional(), diff --git a/packages/miniflare/src/plugins/d1/index.ts b/packages/miniflare/src/plugins/d1/index.ts index cff17189621..1ed036e7fa9 100644 --- a/packages/miniflare/src/plugins/d1/index.ts +++ b/packages/miniflare/src/plugins/d1/index.ts @@ -27,6 +27,7 @@ export const D1OptionsSchema = z.object({ d1Databases: z .union([ z.record( + z.string(), z.union([ z.string(), z.object({ diff --git a/packages/miniflare/src/plugins/dispatch-namespace/index.ts b/packages/miniflare/src/plugins/dispatch-namespace/index.ts index 11e4c776312..61fb08715bb 100644 --- a/packages/miniflare/src/plugins/dispatch-namespace/index.ts +++ b/packages/miniflare/src/plugins/dispatch-namespace/index.ts @@ -12,6 +12,7 @@ import type { Plugin, RemoteProxyConnectionString } from "../shared"; export const DispatchNamespaceOptionsSchema = z.object({ dispatchNamespaces: z .record( + z.string(), z.object({ namespace: z.string(), remoteProxyConnectionString: z diff --git a/packages/miniflare/src/plugins/do/index.ts b/packages/miniflare/src/plugins/do/index.ts index e97ae2d580c..9a847d17648 100644 --- a/packages/miniflare/src/plugins/do/index.ts +++ b/packages/miniflare/src/plugins/do/index.ts @@ -29,7 +29,12 @@ const DurableObject = z.object({ // another `workerd` process, to ensure the IDs created by the stub // object can be used by the real object too. unsafeUniqueKey: z - .union([z.string(), z.literal(kUnsafeEphemeralUniqueKey)]) + .union([ + z.string(), + z.custom( + (v) => v === kUnsafeEphemeralUniqueKey + ), + ]) .optional(), // Prevents the Durable Object being evicted. unsafePreventEviction: z.boolean().optional(), @@ -40,7 +45,9 @@ const DurableObject = z.object({ }); export const DurableObjectsOptionsSchema = z.object({ - durableObjects: z.record(z.union([z.string(), DurableObject])).optional(), + durableObjects: z + .record(z.string(), z.union([z.string(), DurableObject])) + .optional(), // Not all DOs are configured as bindings! Include these in a different key // These might just be configured via migrations, but should still be allocated storage for e.g. ctx.exports support additionalUnboundDurableObjects: z.array(DurableObject).optional(), diff --git a/packages/miniflare/src/plugins/flagship/index.ts b/packages/miniflare/src/plugins/flagship/index.ts index 8c30d56aa3c..16f0f9029bb 100644 --- a/packages/miniflare/src/plugins/flagship/index.ts +++ b/packages/miniflare/src/plugins/flagship/index.ts @@ -15,7 +15,7 @@ const FlagshipSchema = z.object({ }); export const FlagshipOptionsSchema = z.object({ - flagship: z.record(FlagshipSchema).optional(), + flagship: z.record(z.string(), FlagshipSchema).optional(), }); export const FLAGSHIP_PLUGIN_NAME = "flagship"; diff --git a/packages/miniflare/src/plugins/hello-world/index.ts b/packages/miniflare/src/plugins/hello-world/index.ts index 8b008321975..3b3d1fc1103 100644 --- a/packages/miniflare/src/plugins/hello-world/index.ts +++ b/packages/miniflare/src/plugins/hello-world/index.ts @@ -18,6 +18,7 @@ export const HELLO_WORLD_PLUGIN_NAME = "hello-world"; export const HelloWorldOptionsSchema = z.object({ helloWorld: z .record( + z.string(), z.object({ enable_timer: z.boolean().optional(), }) diff --git a/packages/miniflare/src/plugins/hyperdrive/index.ts b/packages/miniflare/src/plugins/hyperdrive/index.ts index 3530245d164..f27fb59a461 100644 --- a/packages/miniflare/src/plugins/hyperdrive/index.ts +++ b/packages/miniflare/src/plugins/hyperdrive/index.ts @@ -23,46 +23,46 @@ function getPort(url: URL) { } export const HyperdriveSchema = z - .union([z.string().url(), z.instanceof(URL)]) + .union([z.url(), z.instanceof(URL)]) .transform((url, ctx) => { if (typeof url === "string") url = new URL(url); if (url.protocol === "") { ctx.addIssue({ - code: z.ZodIssueCode.custom, + code: "custom", message: "You must specify the database protocol - e.g. 'postgresql'/'mysql'.", }); } else if (!hasPostgresProtocol(url) && !hasMysqlProtocol(url)) { ctx.addIssue({ - code: z.ZodIssueCode.custom, + code: "custom", message: "Only PostgreSQL-compatible or MySQL-compatible databases are currently supported.", }); } if (url.host === "") { ctx.addIssue({ - code: z.ZodIssueCode.custom, + code: "custom", message: "You must provide a hostname or IP address in your connection string - e.g. 'user:password@database-hostname.example.com:5432/databasename", }); } if (url.pathname === "") { ctx.addIssue({ - code: z.ZodIssueCode.custom, + code: "custom", message: "You must provide a database name as the path component - e.g. /postgres", }); } if (url.username === "") { ctx.addIssue({ - code: z.ZodIssueCode.custom, + code: "custom", message: "You must provide a username - e.g. 'user:password@database.example.com:port/databasename'", }); } if (url.password === "") { ctx.addIssue({ - code: z.ZodIssueCode.custom, + code: "custom", message: "You must provide a password - e.g. 'user:password@database.example.com:port/databasename' ", }); diff --git a/packages/miniflare/src/plugins/kv/index.ts b/packages/miniflare/src/plugins/kv/index.ts index 4815fac1be5..014438790f9 100644 --- a/packages/miniflare/src/plugins/kv/index.ts +++ b/packages/miniflare/src/plugins/kv/index.ts @@ -33,6 +33,7 @@ export const KVOptionsSchema = z.object({ kvNamespaces: z .union([ z.record( + z.string(), z.union([ z.string(), z.object({ diff --git a/packages/miniflare/src/plugins/mtls/index.ts b/packages/miniflare/src/plugins/mtls/index.ts index 5977b4dde79..1e5857e9bb9 100644 --- a/packages/miniflare/src/plugins/mtls/index.ts +++ b/packages/miniflare/src/plugins/mtls/index.ts @@ -14,7 +14,7 @@ const MtlsSchema = z.object({ }); export const MtlsOptionsSchema = z.object({ - mtlsCertificates: z.record(MtlsSchema).optional(), + mtlsCertificates: z.record(z.string(), MtlsSchema).optional(), }); export const MTLS_PLUGIN_NAME = "mtls"; diff --git a/packages/miniflare/src/plugins/pipelines/index.ts b/packages/miniflare/src/plugins/pipelines/index.ts index c65fc7f5e3d..aa481d494ab 100644 --- a/packages/miniflare/src/plugins/pipelines/index.ts +++ b/packages/miniflare/src/plugins/pipelines/index.ts @@ -13,6 +13,7 @@ export const PipelineOptionsSchema = z.object({ pipelines: z .union([ z.record( + z.string(), z.union([ z.string(), z.object({ diff --git a/packages/miniflare/src/plugins/queues/index.ts b/packages/miniflare/src/plugins/queues/index.ts index c86552f18c1..ff0afb99d33 100644 --- a/packages/miniflare/src/plugins/queues/index.ts +++ b/packages/miniflare/src/plugins/queues/index.ts @@ -29,20 +29,22 @@ export const QueuesOptionsSchema = z.object({ queueProducers: z .union([ z.record( - QueueProducerOptionsSchema.merge( - z.object({ - remoteProxyConnectionString: z - .custom() - .optional(), - }) - ) + z.string(), + QueueProducerOptionsSchema.extend({ + remoteProxyConnectionString: z + .custom() + .optional(), + }) ), z.string().array(), - z.record(z.string()), + z.record(z.string(), z.string()), ]) .optional(), queueConsumers: z - .union([z.record(QueueConsumerOptionsSchema), z.string().array()]) + .union([ + z.record(z.string(), QueueConsumerOptionsSchema), + z.string().array(), + ]) .optional(), }); diff --git a/packages/miniflare/src/plugins/r2/index.ts b/packages/miniflare/src/plugins/r2/index.ts index db01c782679..85a6f63aa16 100644 --- a/packages/miniflare/src/plugins/r2/index.ts +++ b/packages/miniflare/src/plugins/r2/index.ts @@ -27,6 +27,7 @@ export const R2OptionsSchema = z.object({ r2Buckets: z .union([ z.record( + z.string(), z.union([ z.string(), z.object({ diff --git a/packages/miniflare/src/plugins/ratelimit/index.ts b/packages/miniflare/src/plugins/ratelimit/index.ts index e7a81765d64..b4dd59b73f2 100644 --- a/packages/miniflare/src/plugins/ratelimit/index.ts +++ b/packages/miniflare/src/plugins/ratelimit/index.ts @@ -32,11 +32,11 @@ export const RatelimitConfigSchema = z.object({ limit: z.number().gt(0), // may relax this to be any number in the future - period: z.nativeEnum(PeriodType).optional().default(PeriodType.MINUTE), + period: z.enum(PeriodType).optional().default(PeriodType.MINUTE), }), }); export const RatelimitOptionsSchema = z.object({ - ratelimits: z.record(RatelimitConfigSchema).optional(), + ratelimits: z.record(z.string(), RatelimitConfigSchema).optional(), }); export const RATELIMIT_PLUGIN_NAME = "ratelimit"; diff --git a/packages/miniflare/src/plugins/secret-store/index.ts b/packages/miniflare/src/plugins/secret-store/index.ts index 609c6e8a811..27074a797f4 100644 --- a/packages/miniflare/src/plugins/secret-store/index.ts +++ b/packages/miniflare/src/plugins/secret-store/index.ts @@ -17,6 +17,7 @@ import type { Service, Worker_Binding } from "../../runtime"; import type { Plugin } from "../shared"; const SecretsStoreSecretsSchema = z.record( + z.string(), z.object({ store_id: z.string(), secret_name: z.string(), diff --git a/packages/miniflare/src/plugins/shared/index.ts b/packages/miniflare/src/plugins/shared/index.ts index 13b9ffc1d58..e65ac9cd1a7 100644 --- a/packages/miniflare/src/plugins/shared/index.ts +++ b/packages/miniflare/src/plugins/shared/index.ts @@ -22,10 +22,10 @@ import type { UnsafeUniqueKey } from "./constants"; export const DEFAULT_PERSIST_ROOT = ".mf"; export const PersistenceSchema = z - // Zod checks union types in order, both `z.string().url()` and `PathSchema` + // Zod checks union types in order, both `z.url()` and `PathSchema` // will result in a `string`, but `PathSchema` gets resolved relative to the // closest `rootPath`. - .union([z.boolean(), z.string().url(), PathSchema]) + .union([z.boolean(), z.url(), PathSchema]) .optional(); export type Persistence = z.infer; @@ -140,7 +140,7 @@ export type Plugin< */ export async function loadExternalPlugins( packageName: string -): Promise>> { +): Promise>> { let pluginModule; try { const pluginPath = require.resolve(packageName); diff --git a/packages/miniflare/src/plugins/vectorize/index.ts b/packages/miniflare/src/plugins/vectorize/index.ts index b9e4f585281..f3cf83657ca 100644 --- a/packages/miniflare/src/plugins/vectorize/index.ts +++ b/packages/miniflare/src/plugins/vectorize/index.ts @@ -14,7 +14,7 @@ const VectorizeSchema = z.object({ }); export const VectorizeOptionsSchema = z.object({ - vectorize: z.record(VectorizeSchema).optional(), + vectorize: z.record(z.string(), VectorizeSchema).optional(), }); export const VECTORIZE_PLUGIN_NAME = "vectorize"; diff --git a/packages/miniflare/src/plugins/vpc-networks/index.ts b/packages/miniflare/src/plugins/vpc-networks/index.ts index bc363f2bba3..40eae34f892 100644 --- a/packages/miniflare/src/plugins/vpc-networks/index.ts +++ b/packages/miniflare/src/plugins/vpc-networks/index.ts @@ -22,7 +22,7 @@ const VpcNetworksSchema = z.union([ ]); export const VpcNetworksOptionsSchema = z.object({ - vpcNetworks: z.record(VpcNetworksSchema).optional(), + vpcNetworks: z.record(z.string(), VpcNetworksSchema).optional(), }); export const VPC_NETWORKS_PLUGIN_NAME = "vpc-networks"; diff --git a/packages/miniflare/src/plugins/vpc-services/index.ts b/packages/miniflare/src/plugins/vpc-services/index.ts index 597155d4f6e..a87daf5b578 100644 --- a/packages/miniflare/src/plugins/vpc-services/index.ts +++ b/packages/miniflare/src/plugins/vpc-services/index.ts @@ -14,7 +14,7 @@ const VpcServicesSchema = z.object({ }); export const VpcServicesOptionsSchema = z.object({ - vpcServices: z.record(VpcServicesSchema).optional(), + vpcServices: z.record(z.string(), VpcServicesSchema).optional(), }); export const VPC_SERVICES_PLUGIN_NAME = "vpc-services"; diff --git a/packages/miniflare/src/plugins/websearch/index.ts b/packages/miniflare/src/plugins/websearch/index.ts index 70e29380cf9..db4ab15160c 100644 --- a/packages/miniflare/src/plugins/websearch/index.ts +++ b/packages/miniflare/src/plugins/websearch/index.ts @@ -13,7 +13,7 @@ const WebsearchEntrySchema = z.object({ }); export const WebsearchOptionsSchema = z.object({ - websearch: z.record(WebsearchEntrySchema).optional(), + websearch: z.record(z.string(), WebsearchEntrySchema).optional(), }); export const WEBSEARCH_PLUGIN_NAME = "websearch"; diff --git a/packages/miniflare/src/plugins/worker-loader/index.ts b/packages/miniflare/src/plugins/worker-loader/index.ts index 519a8aa76d0..cc56be412a6 100644 --- a/packages/miniflare/src/plugins/worker-loader/index.ts +++ b/packages/miniflare/src/plugins/worker-loader/index.ts @@ -4,7 +4,7 @@ import type { Plugin } from "../shared"; export const WorkerLoaderConfigSchema = z.object({}); export const WorkerLoaderOptionsSchema = z.object({ - workerLoaders: z.record(WorkerLoaderConfigSchema).optional(), + workerLoaders: z.record(z.string(), WorkerLoaderConfigSchema).optional(), }); export const WORKER_LOADER_PLUGIN_NAME = "worker-loader"; diff --git a/packages/miniflare/src/plugins/workflows/index.ts b/packages/miniflare/src/plugins/workflows/index.ts index a89b5219121..b54ccfdcd6e 100644 --- a/packages/miniflare/src/plugins/workflows/index.ts +++ b/packages/miniflare/src/plugins/workflows/index.ts @@ -16,6 +16,7 @@ import type { Plugin, RemoteProxyConnectionString } from "../shared"; export const WorkflowsOptionsSchema = z.object({ workflows: z .record( + z.string(), z.object({ name: z.string(), className: z.string(), diff --git a/packages/miniflare/src/shared/colour.ts b/packages/miniflare/src/shared/colour.ts deleted file mode 100644 index 6b87d80372f..00000000000 --- a/packages/miniflare/src/shared/colour.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { $ as $colors } from "kleur/colors"; - -const originalEnabled = $colors.enabled; - -// `kleur` is marked as a dev dependency, so will get bundled. We'd still like -// to be able to control whether it's enabled in tests though. Therefore, export -// a function that toggles the enabled state of our bundled version. -export function _forceColour(enabled = originalEnabled) { - $colors.enabled = enabled; -} diff --git a/packages/miniflare/src/shared/index.ts b/packages/miniflare/src/shared/index.ts index 253f2e50a53..b1c29594045 100644 --- a/packages/miniflare/src/shared/index.ts +++ b/packages/miniflare/src/shared/index.ts @@ -1,4 +1,3 @@ -export * from "./colour"; export * from "./error"; export * from "./event"; export * from "./log"; diff --git a/packages/miniflare/src/shared/types.ts b/packages/miniflare/src/shared/types.ts index 4597d1483b7..83ea86c5e2d 100644 --- a/packages/miniflare/src/shared/types.ts +++ b/packages/miniflare/src/shared/types.ts @@ -1,16 +1,14 @@ import assert from "node:assert"; import path from "node:path"; import { z } from "zod"; -import type { ParseParams } from "zod"; - -export function zAwaitable( +export function zAwaitable( type: T ): z.ZodUnion<[T, z.ZodPromise]> { return type.or(z.promise(type)); } -export type OptionalZodTypeOf = - T extends z.ZodTypeAny ? z.TypeOf : undefined; +export type OptionalZodTypeOf = + T extends z.ZodType ? z.output : undefined; // https://github.com/colinhacks/zod/blob/59768246aa57133184b2cf3f7c2a1ba5c3ab08c3/README.md?plain=1#L1302-L1317 export const LiteralSchema = z.union([ @@ -22,19 +20,30 @@ export const LiteralSchema = z.union([ export type Literal = z.infer; export type Json = Literal | { [key: string]: Json } | Json[]; export const JsonSchema: z.ZodType = z.lazy(() => - z.union([LiteralSchema, z.array(JsonSchema), z.record(JsonSchema)]) + z.union([ + LiteralSchema, + z.array(JsonSchema), + z.record(z.string(), JsonSchema), + ]) ); let rootPath: string | undefined; -export function parseWithRootPath( +export function parseWithRootPath( newRootPath: string, schema: Z, data: unknown, - params?: Partial + options?: { path?: (string | number)[] } ): z.infer { rootPath = newRootPath; try { - return schema.parse(data, params); + return schema.parse(data); + } catch (e) { + if (options?.path && e instanceof z.ZodError) { + for (const issue of e.issues) { + issue.path.unshift(...options.path); + } + } + throw e; } finally { rootPath = undefined; } diff --git a/packages/miniflare/src/workers/local-explorer/common.ts b/packages/miniflare/src/workers/local-explorer/common.ts index 7ce298c8215..dae29010a53 100644 --- a/packages/miniflare/src/workers/local-explorer/common.ts +++ b/packages/miniflare/src/workers/local-explorer/common.ts @@ -20,12 +20,14 @@ export type AppContext = Context; * * If the whole query param is optional, you need to unwrap it before passing to this function. */ -export function validateQuery(schema: T) { +export function validateQuery(schema: T) { return validator("query", async (value, c) => { - let result: z.SafeParseReturnType, z.output>; + let result: + | { success: true; data: z.output } + | { success: false; error: z.ZodError }; try { const coerced = coerceValue(schema, value); - result = await schema.safeParseAsync(coerced); + result = (await schema.safeParseAsync(coerced)) as typeof result; } catch (error) { if (error instanceof z.ZodError) { return validationHook({ success: false, error }, c); @@ -42,7 +44,7 @@ export function validateQuery(schema: T) { /** * validates request body according to openapi schema */ -export function validateRequestBody(schema: T) { +export function validateRequestBody(schema: T) { return validator("json", async (value, c) => { const result = await schema.safeParseAsync(value); if (!result.success) { @@ -63,14 +65,14 @@ export function validateRequestBody(schema: T) { * 3. Arrays/Objects: We need to recursively coerce nested values */ export function coerceValue( - schema: z.ZodTypeAny, + schema: z.ZodType, value: unknown, path: (string | number)[] = [] ): unknown { // Unwrap optional/default to get inner type if (schema instanceof z.ZodOptional || schema instanceof z.ZodDefault) { if (value === undefined) return value; - return coerceValue(schema._def.innerType, value, path); + return coerceValue(schema._zod.def.innerType as z.ZodType, value, path); } if (schema instanceof z.ZodNumber && typeof value === "string") { @@ -78,9 +80,8 @@ export function coerceValue( if (isNaN(num)) { throw new z.ZodError([ { - code: z.ZodIssueCode.invalid_type, + code: "invalid_type", expected: "number", - received: "string", path, message: `Expected query param to be number but received "${value}"`, }, @@ -94,9 +95,8 @@ export function coerceValue( if (value === "false") return false; throw new z.ZodError([ { - code: z.ZodIssueCode.invalid_type, + code: "invalid_type", expected: "boolean", - received: "string", path, message: `Expected query param to be 'true' or 'false' but received "${value}"`, }, @@ -105,7 +105,7 @@ export function coerceValue( if (schema instanceof z.ZodArray && Array.isArray(value)) { return value.map((item, index) => - coerceValue(schema.element, item, [...path, index]) + coerceValue(schema.element as z.ZodType, item, [...path, index]) ); } @@ -118,7 +118,7 @@ export function coerceValue( for (const [key, propSchema] of Object.entries(schema.shape)) { if (key in value) { result[key] = coerceValue( - propSchema as z.ZodTypeAny, + propSchema as z.ZodType, (value as Record)[key], [...path, key] ); @@ -136,7 +136,7 @@ export function validationHook( result: { success: false; error: z.ZodError }, c: Context ): Response { - const errors = result.error.errors.map((e) => { + const errors = result.error.issues.map((e) => { const message = e.path.length > 0 ? `${e.path.join(".")}: ${e.message}` : e.message; diff --git a/packages/miniflare/src/workers/local-explorer/generated/zod.gen.ts b/packages/miniflare/src/workers/local-explorer/generated/zod.gen.ts index a9a0769ff60..5d30d46326a 100644 --- a/packages/miniflare/src/workers/local-explorer/generated/zod.gen.ts +++ b/packages/miniflare/src/workers/local-explorer/generated/zod.gen.ts @@ -1,12 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts -import { z } from "zod"; +import * as z from "zod"; export const zR2Messages = z.array(z.string()); export const zR2Errors = z.array( z.object({ - code: z.number().int().gte(1000), + code: z.int().gte(1000), message: z.string(), }) ); @@ -14,7 +14,7 @@ export const zR2Errors = z.array( export const zR2V4Response = z.object({ errors: zR2Errors, messages: zR2Messages, - result: z.record(z.unknown()), + result: z.record(z.string(), z.unknown()), success: z.literal(true), }); @@ -42,7 +42,7 @@ export const zR2Bucket = z.object({ name: zR2BucketName.optional(), }); -export const zR2ResultInfo = z.record(z.unknown()); +export const zR2ResultInfo = z.record(z.string(), z.unknown()); export const zR2V4ResponseList = zR2V4Response.and( z.object({ @@ -68,7 +68,7 @@ export const zWorkersSchemasId = z.string(); export const zWorkersMessages = z.array( z.object({ - code: z.number().int().gte(1000), + code: z.int().gte(1000), documentation_url: z.string().optional(), message: z.string(), source: z @@ -136,7 +136,9 @@ export const zD1RawResultResponse = z.object({ columns: z.array(z.string()).optional(), rows: z .array( - z.array(z.union([z.number(), z.string(), z.record(z.unknown())])) + z.array( + z.union([z.number(), z.string(), z.record(z.string(), z.unknown())]) + ) ) .optional(), }) @@ -178,7 +180,7 @@ export const zD1DatabaseIdentifier = z.string().readonly(); export const zD1Messages = z.array( z.object({ - code: z.number().int().gte(1000), + code: z.int().gte(1000), message: z.string(), }) ); @@ -215,14 +217,14 @@ export const zD1ApiResponseCommon = z.object({ success: z.literal(true), }); -export const zWorkersKvAny: z.ZodTypeAny = z +export const zWorkersKvAny = z .union([ z.string(), z.number(), - z.number().int(), + z.int(), z.boolean(), - z.record(z.unknown()), - z.array(z.lazy(() => zWorkersKvAny)), + z.record(z.string(), z.unknown()), + z.array(z.lazy((): any => zWorkersKvAny)), ]) .nullable(); @@ -234,6 +236,7 @@ export const zWorkersKvExpiration = z.number(); export const zWorkersKvBulkGetResultWithMetadata = z.object({ values: z .record( + z.string(), z .object({ expiration: zWorkersKvExpiration.optional(), @@ -248,8 +251,14 @@ export const zWorkersKvBulkGetResultWithMetadata = z.object({ export const zWorkersKvBulkGetResult = z.object({ values: z .record( + z.string(), z - .union([z.string(), z.number(), z.boolean(), z.record(z.unknown())]) + .union([ + z.string(), + z.number(), + z.boolean(), + z.record(z.string(), z.unknown()), + ]) .nullable() ) .optional(), @@ -262,7 +271,7 @@ export const zWorkersKvKeyNameBulk = z.string().max(512); export const zWorkersKvMessages = z.array( z.object({ - code: z.number().int().gte(1000), + code: z.int().gte(1000), message: z.string(), }) ); @@ -276,7 +285,7 @@ export const zWorkersKvApiResponseCommon = z.object({ export const zWorkersKvApiResponseCommonNoResult = zWorkersKvApiResponseCommon.and( z.object({ - result: z.record(z.unknown()).nullish(), + result: z.record(z.string(), z.unknown()).nullish(), }) ); @@ -321,7 +330,7 @@ export const zWorkersKvNamespaceIdentifier = z.string().max(32).readonly(); export const zWorkersKvApiResponseCommonFailure = z.object({ errors: zWorkersKvMessages, messages: zWorkersKvMessages, - result: z.record(z.unknown()).nullable(), + result: z.record(z.string(), z.unknown()).nullable(), success: z.literal(false), }); @@ -348,10 +357,10 @@ export const zWorkersKvApiResponseCollection = zWorkersKvApiResponseCommon.and( export const zR2Object = z.object({ key: z.string().optional(), etag: z.string().optional(), - size: z.number().int().optional(), - last_modified: z.string().datetime().optional(), - http_metadata: z.record(z.string()).optional(), - custom_metadata: z.record(z.string()).optional(), + size: z.int().optional(), + last_modified: z.iso.datetime().optional(), + http_metadata: z.record(z.string(), z.string()).optional(), + custom_metadata: z.record(z.string(), z.string()).optional(), }); export const zR2ListObjectsResultInfo = z.object({ @@ -364,15 +373,15 @@ export const zR2HeadObjectResult = z.object({ key: z.string().optional(), etag: z.string().optional(), last_modified: z.string().optional(), - size: z.number().int().optional(), - http_metadata: z.record(z.string()).optional(), - custom_metadata: z.record(z.string()).optional(), + size: z.int().optional(), + http_metadata: z.record(z.string(), z.string()).optional(), + custom_metadata: z.record(z.string(), z.string()).optional(), }); export const zR2PutObjectResult = z.object({ key: z.string().optional(), etag: z.string().optional(), - size: z.number().int().optional(), + size: z.int().optional(), version: z.string().optional(), }); @@ -511,7 +520,7 @@ export const zWorkflowsInstanceDetails = z.object({ .optional(), }); -export const zR2ResultInfoWritable = z.record(z.unknown()); +export const zR2ResultInfoWritable = z.record(z.string(), z.unknown()); export const zWorkersNamespaceWritable = z.object({ class: z.string().optional(), @@ -526,14 +535,14 @@ export const zD1DatabaseResponseWritable = z.object({ version: zD1DatabaseVersion.optional(), }); -export const zWorkersKvAnyWritable: z.ZodTypeAny = z +export const zWorkersKvAnyWritable = z .union([ z.string(), z.number(), - z.number().int(), + z.int(), z.boolean(), - z.record(z.unknown()), - z.array(z.lazy(() => zWorkersKvAnyWritable)), + z.record(z.string(), z.unknown()), + z.array(z.lazy((): any => zWorkersKvAnyWritable)), ]) .nullable(); @@ -816,7 +825,7 @@ export const zR2BucketListObjectsData = z.object({ prefix: z.string().optional(), delimiter: z.string().optional(), cursor: z.string().optional(), - per_page: z.number().int().optional().default(1000), + per_page: z.int().optional().default(1000), }) .optional(), }); @@ -1086,7 +1095,7 @@ export const zWorkflowsChangeInstanceStatusData = z.object({ from: z .object({ name: z.string(), - count: z.number().int().gte(1).optional(), + count: z.int().gte(1).optional(), type: z.enum(["do", "sleep", "waitForEvent"]).optional(), }) .optional(), diff --git a/packages/miniflare/src/workers/queues/schemas.ts b/packages/miniflare/src/workers/queues/schemas.ts index 985c359a72a..458d8f8f8de 100644 --- a/packages/miniflare/src/workers/queues/schemas.ts +++ b/packages/miniflare/src/workers/queues/schemas.ts @@ -19,7 +19,7 @@ export const QueueProducerSchema = /* @__PURE__ */ z.intersection( ); export type QueueProducer = z.infer; export const QueueProducersSchema = - /* @__PURE__ */ z.record(QueueProducerSchema); + /* @__PURE__ */ z.record(z.string(), QueueProducerSchema); export const QueueConsumerOptionsSchema = /* @__PURE__ */ z.object({ // https://developers.cloudflare.com/queues/platform/configuration/#consumer @@ -27,7 +27,7 @@ export const QueueConsumerOptionsSchema = /* @__PURE__ */ z.object({ maxBatchSize: z.number().min(0).max(100).optional(), maxBatchTimeout: z.number().min(0).max(60).optional(), // seconds maxRetries: z.number().min(0).max(100).optional(), - deadLetterQueue: z.ostring(), + deadLetterQueue: z.string().optional(), retryDelay: QueueMessageDelaySchema, }); export const QueueConsumerSchema = /* @__PURE__ */ z.intersection( @@ -40,7 +40,7 @@ export type QueueConsumer = z.infer; // queues. Support for multiple consumers of a single queue is not planned // anytime soon. export const QueueConsumersSchema = - /* @__PURE__ */ z.record(QueueConsumerSchema); + /* @__PURE__ */ z.record(z.string(), QueueConsumerSchema); export const QueueContentTypeSchema = /* @__PURE__ */ z .enum(["text", "json", "bytes", "v8"]) @@ -55,8 +55,8 @@ export const QueueIncomingMessageSchema = /* @__PURE__ */ z.object({ body: Base64DataSchema, // When enqueuing messages on dead-letter queues, we want to reuse the same ID // and timestamp - id: z.ostring(), - timestamp: z.onumber(), + id: z.string().optional(), + timestamp: z.number().optional(), }); export type QueueIncomingMessage = z.infer; export type QueueOutgoingMessage = z.input; diff --git a/packages/miniflare/src/workers/r2/schemas.worker.ts b/packages/miniflare/src/workers/r2/schemas.worker.ts index 2cd2c8177f0..3ce0d31c9e1 100644 --- a/packages/miniflare/src/workers/r2/schemas.worker.ts +++ b/packages/miniflare/src/workers/r2/schemas.worker.ts @@ -113,7 +113,7 @@ export const R2ConditionalSchema = z.object({ // Performs the operation if the object was uploaded AFTER the given date uploadedAfter: DateSchema.optional(), // "If-Modified-Since" // Truncates dates to seconds before performing comparisons - secondsGranularity: z.oboolean(), + secondsGranularity: z.boolean().optional(), }); export type R2Conditional = z.infer; @@ -142,11 +142,11 @@ export const R2PublishedPartSchema = z.object({ export type R2PublishedPart = z.infer; export const R2HttpFieldsSchema = z.object({ - contentType: z.ostring(), - contentLanguage: z.ostring(), - contentDisposition: z.ostring(), - contentEncoding: z.ostring(), - cacheControl: z.ostring(), + contentType: z.string().optional(), + contentLanguage: z.string().optional(), + contentDisposition: z.string().optional(), + contentEncoding: z.string().optional(), + cacheControl: z.string().optional(), cacheExpiry: z.coerce.number().optional(), }); export type R2HttpFields = z.infer; @@ -163,7 +163,7 @@ export const R2GetRequestSchema = z.object({ // of bytes from the object should be returned. Refer to // https://developers.cloudflare.com/r2/runtime-apis/#ranged-reads. range: R2RangeSchema.optional(), - rangeHeader: z.ostring(), + rangeHeader: z.string().optional(), // Specifies that the object should only be returned given satisfaction of // certain conditions in the R2Conditional. Refer to R2Conditional above. onlyIf: R2ConditionalSchema.optional(), @@ -231,11 +231,11 @@ export const R2AbortMultipartUploadRequestSchema = z.object({ export const R2ListRequestSchema = z.object({ method: z.literal("list"), - limit: z.onumber(), - prefix: z.ostring(), - cursor: z.ostring(), - delimiter: z.ostring(), - startAfter: z.ostring(), + limit: z.number().optional(), + prefix: z.string().optional(), + cursor: z.string().optional(), + delimiter: z.string().optional(), + startAfter: z.string().optional(), include: z .union([z.literal(0), z.literal(1)]) .transform((value) => (value === 0 ? "httpMetadata" : "customMetadata")) diff --git a/packages/miniflare/test/index.spec.ts b/packages/miniflare/test/index.spec.ts index 0e76d185168..582fe1a86e8 100644 --- a/packages/miniflare/test/index.spec.ts +++ b/packages/miniflare/test/index.spec.ts @@ -12,8 +12,8 @@ import path from "node:path"; import { json, text } from "node:stream/consumers"; import url from "node:url"; import util from "node:util"; +import { _forceColour } from "@cloudflare/workers-utils"; import { - _forceColour, _transformsForContentEncodingAndContentType, createFetchMock, DeferredPromise, @@ -123,7 +123,7 @@ test("Miniflare: validates options", async ({ expect, onTestFinished }) => { `Unexpected options passed to \`new Miniflare()\` constructor: { name: 42, - ^ Expected string, received number + ^ Invalid input: expected string, received number ..., }` ); @@ -141,7 +141,7 @@ test("Miniflare: validates options", async ({ expect, onTestFinished }) => { expect(error?.message).toEqual( `Unexpected options passed to \`new Miniflare()\` constructor: 'addEventListener(...)' -^ Expected object, received string` +^ Invalid input: expected object, received string` ); }); diff --git a/packages/miniflare/test/plugins/local-explorer/helpers.ts b/packages/miniflare/test/plugins/local-explorer/helpers.ts index 414cac93dac..998dd80f2d5 100644 --- a/packages/miniflare/test/plugins/local-explorer/helpers.ts +++ b/packages/miniflare/test/plugins/local-explorer/helpers.ts @@ -1,11 +1,11 @@ +import { z } from "zod"; import type { ExpectStatic } from "vitest"; -import type { z } from "zod"; /** * Validates a response body against a Zod schema and returns typed data. * Throws a descriptive error if validation fails. */ -export async function expectValidResponse( +export async function expectValidResponse( response: Response, schema: T, expect: ExpectStatic, @@ -18,7 +18,7 @@ export async function expectValidResponse( if (!result.success) { throw new Error( `Response validation failed:\n${JSON.stringify( - result.error.format(), + z.treeifyError(result.error), null, 2 )}\n\nActual response:\n${JSON.stringify(json, null, 2)}` diff --git a/packages/miniflare/test/plugins/local-explorer/index.spec.ts b/packages/miniflare/test/plugins/local-explorer/index.spec.ts index 5a3941d6af1..8fa26ece0c2 100644 --- a/packages/miniflare/test/plugins/local-explorer/index.spec.ts +++ b/packages/miniflare/test/plugins/local-explorer/index.spec.ts @@ -66,7 +66,7 @@ describe("Local Explorer API validation", () => { errors: [ { code: 10001, - message: "limit: Number must be greater than or equal to 10", + message: "limit: Too small: expected number to be >=10", }, ], }); @@ -83,7 +83,7 @@ describe("Local Explorer API validation", () => { errors: [ { code: 10001, - message: "limit: Number must be less than or equal to 1000", + message: "limit: Too big: expected number to be <=1000", }, ], }); @@ -107,7 +107,7 @@ describe("Local Explorer API validation", () => { errors: [ { code: 10001, - message: "keys: Expected array, received string", + message: "keys: Invalid input: expected array, received string", }, ], }); @@ -129,7 +129,7 @@ describe("Local Explorer API validation", () => { errors: [ { code: 10001, - message: "keys: Required", + message: "keys: Invalid input: expected array, received undefined", }, ], }); diff --git a/packages/miniflare/test/plugins/queues/index.spec.ts b/packages/miniflare/test/plugins/queues/index.spec.ts index ad2532b6053..42be440d041 100644 --- a/packages/miniflare/test/plugins/queues/index.spec.ts +++ b/packages/miniflare/test/plugins/queues/index.spec.ts @@ -64,7 +64,7 @@ test("maxBatchTimeout validation", async ({ expect }) => { error = e as MiniflareCoreError; } expect(error?.code).toEqual("ERR_VALIDATION"); - expect(error?.message).toMatch(/Number must be less than or equal to 60/); + expect(error?.message).toMatch(/Too big: expected number to be <=60/); }); test("flushes partial and full batches", async ({ expect }) => { diff --git a/packages/miniflare/test/snapshots/zod-format.spec.ts.md b/packages/miniflare/test/snapshots/zod-format.spec.ts.md deleted file mode 100644 index ffad9d4bf0f..00000000000 --- a/packages/miniflare/test/snapshots/zod-format.spec.ts.md +++ /dev/null @@ -1,307 +0,0 @@ -# Snapshot report for `test/zod-format.spec.ts` - -The actual snapshot is saved in `zod-format.spec.ts.snap`. - -Generated by [AVA](https://avajs.dev). - -## formatZodError: formats primitive schema with primitive input - -> Snapshot 1 - - `false␊ - ^ Expected number, received boolean` - -## formatZodError: formats primitive schema with object input - -> Snapshot 1 - - `{ a: 1, b: [Object] }␊ - ^ Expected string, received object` - -## formatZodError: formats object schema with primitive input - -> Snapshot 1 - - `true␊ - ^ Expected object, received boolean` - -## formatZodError: formats object schema with object input - -> Snapshot 1 - - `{␊ - ...,␊ - b: '2',␊ - ^ Expected number, received string␊ - ...,␊ - g: '7',␊ - ^ Expected boolean, received string␊ - f: undefined,␊ - ^ Required␊ - }` - -## formatZodError: formats object schema with additional options - -> Snapshot 1 - - `{ a: 1, b: 2 }␊ - ^ Unrecognized key(s) in object: 'b'` - -## formatZodError: formats array schema with primitive input - -> Snapshot 1 - - `1␊ - ^ Expected array, received number` - -## formatZodError: formats array schema with array input - -> Snapshot 1 - - `[␊ - ...,␊ - /* [2] */ '3',␊ - ^ Expected number, received string␊ - ...,␊ - /* [5] */ false,␊ - ^ Expected number, received boolean␊ - ]` - -## formatZodError: formats array schema with additional options - -> Snapshot 1 - - `[ 1, 2, 3, 4, 5 ]␊ - ^ Array must contain at most 3 element(s)` - -## formatZodError: formats deeply nested schema - -> Snapshot 1 - - `{␊ - a: '1',␊ - ^ Expected number, received string␊ - b: {␊ - c: 2,␊ - ^ Expected string, received number␊ - d: [␊ - ...,␊ - /* [1] */ {␊ - e: 42,␊ - ^ Expected boolean, received number␊ - },␊ - /* [2] */ false,␊ - ^ Expected object, received boolean␊ - /* [3] */ {␊ - e: undefined,␊ - ^ Required␊ - },␊ - ],␊ - f: [Function: f],␊ - ^ Expected array, received function␊ - },␊ - g: undefined,␊ - ^ Required␊ - }` - -## formatZodError: formats large actual values - -> Snapshot 1 - - `{␊ - a: {␊ - b: [␊ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,␊ - 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,␊ - 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32,␊ - 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43,␊ - 44, 45, 46, 47, 48, 49␊ - ],␊ - ^ Expected string, received array␊ - },␊ - }` - -## formatZodError: formats union schema - -> Snapshot 1 - - `'a'␊ - ^ Expected boolean, received string␊ - Invalid literal value, expected 1` - -## formatZodError: formats discriminated union schema - -> Snapshot 1 - - `{␊ - ...,␊ - a: false,␊ - ^ Expected number, received boolean␊ - }` - -## formatZodError: formats discriminated union schema with invalid discriminator - -> Snapshot 1 - - `{␊ - type: 'c',␊ - ^ Invalid discriminator value. Expected 'a' | 'b'␊ - }` - -## formatZodError: formats intersection schema - -> Snapshot 1 - - `false␊ - ^ Expected number, received boolean␊ - Invalid literal value, expected 2` - -## formatZodError: formats object union schema - -> Snapshot 1 - - `{␊ - key: false,␊ - ^ Expected string, received boolean␊ - objects: [␊ - /* [0] */ false,␊ - ^ Expected object, received boolean␊ - ...,␊ - /* [2] */ {␊ - a: undefined,␊ - ^1 Required *or*␊ - b: undefined,␊ - ^1 Required *or*␊ - c: undefined,␊ - ^1 Required␊ - },␊ - /* [3] */ [],␊ - ^ Expected object, received array␊ - /* [4] */ {␊ - ...,␊ - a: undefined,␊ - ^2 Required *or*␊ - b: undefined,␊ - ^2 Required *or*␊ - c: undefined,␊ - ^2 Required␊ - },␊ - ],␊ - }` - -## formatZodError: formats object union schema in colour - -> Snapshot 1 - - `{␊ - key: false,␊ -  ^ Expected string, received boolean␊ - objects: [␊ - /* [0] */ false,␊ -  ^ Expected object, received boolean␊ - /* [1] */ {␊ - a: undefined,␊ -  ^1 Required *or*␊ - b: undefined,␊ -  ^1 Required *or*␊ - c: undefined,␊ -  ^1 Required␊ - },␊ - /* [2] */ {␊ - a: undefined,␊ -  ^2 Required *or*␊ - b: undefined,␊ -  ^2 Required *or*␊ - c: undefined,␊ -  ^2 Required␊ - },␊ - /* [3] */ {␊ - a: undefined,␊ -  ^3 Required *or*␊ - b: undefined,␊ -  ^3 Required *or*␊ - c: undefined,␊ -  ^3 Required␊ - },␊ - /* [4] */ {␊ - a: undefined,␊ -  ^4 Required *or*␊ - b: undefined,␊ -  ^4 Required *or*␊ - c: undefined,␊ -  ^4 Required␊ - },␊ - /* [5] */ {␊ - a: undefined,␊ -  ^5 Required *or*␊ - b: undefined,␊ -  ^5 Required *or*␊ - c: undefined,␊ -  ^5 Required␊ - },␊ - /* [6] */ {␊ - a: undefined,␊ -  ^6 Required *or*␊ - b: undefined,␊ -  ^6 Required *or*␊ - c: undefined,␊ -  ^6 Required␊ - },␊ - /* [7] */ {␊ - a: undefined,␊ -  ^7 Required *or*␊ - b: undefined,␊ -  ^7 Required *or*␊ - c: undefined,␊ -  ^7 Required␊ - },␊ - ],␊ - }` - -## formatZodError: formats tuple union schema - -> Snapshot 1 - - `{␊ - tuples: [␊ - /* [0] */ false,␊ - ^ Expected array, received boolean␊ - /* [1] */ { a: 1 },␊ - ^ Expected array, received object␊ - /* [2] */ [],␊ - ^ Array must contain at least 2 element(s)␊ - Array must contain at least 3 element(s)␊ - /* [3] */ [␊ - ...,␊ - /* [1] */ '3',␊ - ^ Expected number, received string␊ - ],␊ - /* [4] */ [␊ - /* [0] */ 4,␊ - ^1 Expected string, received number␊ - Expected boolean, received number *or*␊ - /* [1] */ 5,␊ - ^1 Expected boolean, received number *or*␊ - /* [2] */ 6,␊ - ^1 Expected boolean, received number␊ - ],␊ - /* [5] */ [␊ - /* [0] */ true,␊ - ^2 Expected string, received boolean *or*␊ - /* [1] */ 7,␊ - ^2 Expected boolean, received number␊ - ...,␊ - ],␊ - ],␊ - }` - -## formatZodError: formats custom message schema - -> Snapshot 1 - - `{␊ - a: Symbol(kOoh),␊ - ^ Custom message␊ - with multiple␊ - lines␊ - }` diff --git a/packages/miniflare/test/snapshots/zod-format.spec.ts.snap b/packages/miniflare/test/snapshots/zod-format.spec.ts.snap deleted file mode 100644 index 3c9c0e27d67a910087781a9d2d1adddb98514628..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1517 zcmV54`XL5aPWc{uQr0@yZY2%wA{Kdrh2`oOmzaH{YD|&DoR5`^DUH z^$Y*kKUiP}7JK*k!u4z#eCHa^JkRweC_eYm$}?>*vg-=&c!?3s+5N6$Ym7 z(3OAg2L8|Q4TA>sWVE~W;TAu(KiK+cdw)SKpY_hrvv(_|2h2cDXwR9aAdl$`Xbd!W zU5imC)idd)$DRKGyj5bJOJW{hAv!^-g60!6eH8&`=vox#2cGFH1#vNbqqamvr$7xQ zZF_+iN^y!QtK&54prt-Ye~ChNe&4$4A#`|nsK74>R3G>GH#q8&7zZu~E&(_y0Vb0u zLoOyLbPTpI9cE;}ui5L+^qA4RZuD!(ys2S83S5U;$X)R-{`%~nN@*a1MR4SeZyk{8 zE*Gj#9>^+}rJ+v zUEe1ucF6wFn1R2y7CuuMNb+~GZRRXKYs8Ws!Gb} zB6+@-kS*I4x`$Psv8cCXi0VNICVoS$kga?8bo-@bnJ1}b;te0H^>^0iXgv1%L_w6#yy#Fc4!^0AT=N0AK)M0AK)M z0AK)M01yBWASR-J2mlBG2mlBG2mlBG2mmwyXaE4iA&m$i4FDPdGyrG-&;X!~)BB}) zTXTPu$T(|RRz9xEO6Wj}t5)#IsZ>(F>eGI&-COa60~t0AWSIf;(tIn3rBF2$hAT^q z>FfM;<52#5z06}t<`bb40PBbgu}yi-Vs_kJT3oEnWb(puuD7oGL{j}FR~=lgV2Ss2 z;q`Qu#Zsb+BYK$62Xcdc;_vkqOVM;7D?W>scV$qglDy;A9dV27<3@!^S5%w!(;X}o ziw8SAgwX3u3dOf>vE-uTA^o((@bY_9UCGio{Cy^ne3-_ml*pZvy+t}temH0_GSMBz0jo=A=ZrgOw z_aO@4f2XQ?U?L2?mAQ|E)lobx;2)NjWr$KzUAPX@ka ze1^ifcti2&z}Y%J<($p&A-9Z=CX7oq6rT>9t>aV9*&HA3mhl+~<7yjE^XLxha|7p%>hq{E zKKyK!f#FW{HSMAfs=BBkTv@E4ec$~>YMtKhMt=cu;0>Da;_ijmgA3hBv`w0C*R&vt z8=yi9U)Cm<@2!CdxPXPtwY4pwIv^p zsur^j)^u%TGvB96R=HvDy|iKQGTFKN=F* { - test("formats primitive schema with primitive input", ({ expect }) => { - const formatted = formatZodErrorForTest(z.number(), false); - expect(formatted).toMatchInlineSnapshot(` - "false - ^ Expected number, received boolean" - `); - }); - test("formats primitive schema with object input", ({ expect }) => { - const formatted = formatZodErrorForTest(z.string(), { - a: 1, - b: { c: 1 }, - }); - expect(formatted).toMatchInlineSnapshot(` - "{ a: 1, b: [Object] } - ^ Expected string, received object" - `); - }); - - test("formats object schema with primitive input", ({ expect }) => { - const formatted = formatZodErrorForTest(z.object({ a: z.number() }), true); - expect(formatted).toMatchInlineSnapshot(` - "true - ^ Expected object, received boolean" - `); - }); - test("formats object schema with object input", ({ expect }) => { - const formatted = formatZodErrorForTest( - z.object({ - a: z.string(), - b: z.number(), - c: z.boolean(), - d: z.number(), - e: z.number(), - f: z.boolean(), - g: z.boolean(), - }), - { - a: "", // Check skips valid - b: "2", - c: true, // Check skips valid - d: 4, // Check doesn't duplicate `...` when skipping valid - e: 5, - /*f*/ // Check required options - g: "7", - } - ); - expect(formatted).toMatchInlineSnapshot(` - "{ - ..., - b: '2', - ^ Expected number, received string - ..., - g: '7', - ^ Expected boolean, received string - f: undefined, - ^ Required - }" - `); - }); - test("formats object schema with additional options", ({ expect }) => { - const formatted = formatZodErrorForTest( - z.object({ a: z.number() }).strict(), - { a: 1, b: 2 } - ); - expect(formatted).toMatchInlineSnapshot(` - "{ a: 1, b: 2 } - ^ Unrecognized key(s) in object: 'b'" - `); - }); - - test("formats array schema with primitive input", ({ expect }) => { - const formatted = formatZodErrorForTest(z.array(z.boolean()), 1); - expect(formatted).toMatchInlineSnapshot(` - "1 - ^ Expected array, received number" - `); - }); - test("formats array schema with array input", ({ expect }) => { - const formatted = formatZodErrorForTest(z.array(z.number()), [ - 1, // Check skips valid - 2, // Check doesn't duplicate `...` when skipping valid - "3", - 4, - 5, - false, - ]); - expect(formatted).toMatchInlineSnapshot(` - "[ - ..., - /* [2] */ '3', - ^ Expected number, received string - ..., - /* [5] */ false, - ^ Expected number, received boolean - ]" - `); - }); - test("formats array schema with additional options", ({ expect }) => { - const formatted = formatZodErrorForTest( - z.array(z.number()).max(3), - [1, 2, 3, 4, 5] - ); - expect(formatted).toMatchInlineSnapshot(` - "[ 1, 2, 3, 4, 5 ] - ^ Array must contain at most 3 element(s)" - `); - }); - - test("formats deeply nested schema", ({ expect }) => { - const formatted = formatZodErrorForTest( - z.object({ - a: z.number(), - b: z.object({ - c: z.string(), - d: z.array(z.object({ e: z.boolean() })), - f: z.array(z.number()), - }), - g: z.string(), - }), - { - a: "1", - b: { - c: 2, - d: [{ e: true }, { e: 42 }, false, {}], - f: () => {}, - }, - } - ); - expect(formatted).toMatchInlineSnapshot(` - "{ - a: '1', - ^ Expected number, received string - b: { - c: 2, - ^ Expected string, received number - d: [ - ..., - /* [1] */ { - e: 42, - ^ Expected boolean, received number - }, - /* [2] */ false, - ^ Expected object, received boolean - /* [3] */ { - e: undefined, - ^ Required - }, - ], - f: [Function: f], - ^ Expected array, received function - }, - g: undefined, - ^ Required - }" - `); - }); - - test("formats large actual values", ({ expect }) => { - const formatted = formatZodErrorForTest( - z.object({ - a: z.object({ - b: z.string(), - }), - }), - { - a: { - // Check indents inspected value at correct depth - b: Array.from({ length: 50 }).map((_, i) => i), - }, - } - ); - expect(formatted).toMatchInlineSnapshot(` - "{ - a: { - b: [ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, - 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, - 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, - 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, - 44, 45, 46, 47, 48, 49 - ], - ^ Expected string, received array - }, - }" - `); - }); - - test("formats union schema", ({ expect }) => { - const formatted = formatZodErrorForTest( - z.union([z.boolean(), z.literal(1)]), - "a" - ); - expect(formatted).toMatchInlineSnapshot(` - "'a' - ^ Expected boolean, received string - Invalid literal value, expected 1" - `); - }); - - const discriminatedUnionSchema = z.discriminatedUnion("type", [ - z.object({ - type: z.literal("a"), - a: z.number(), - }), - z.object({ - type: z.literal("b"), - b: z.boolean(), - }), - ]); - test("formats discriminated union schema", ({ expect }) => { - const formatted = formatZodErrorForTest(discriminatedUnionSchema, { - type: "a", - a: false, - }); - expect(formatted).toMatchInlineSnapshot(` - "{ - ..., - a: false, - ^ Expected number, received boolean - }" - `); - }); - test("formats discriminated union schema with invalid discriminator", ({ - expect, - }) => { - const formatted = formatZodErrorForTest(discriminatedUnionSchema, { - type: "c", - }); - expect(formatted).toMatchInlineSnapshot(` - "{ - type: 'c', - ^ Invalid discriminator value. Expected 'a' | 'b' - }" - `); - }); - - test("formats intersection schema", ({ expect }) => { - const formatted = formatZodErrorForTest( - z.intersection(z.number(), z.literal(2)), - false - ); - expect(formatted).toMatchInlineSnapshot(` - "false - ^ Expected number, received boolean - Invalid literal value, expected 2" - `); - }); - - const objectUnionSchema = z.object({ - key: z.string(), - objects: z.array( - z.union([ - z.object({ a: z.number() }), - z.object({ b: z.boolean() }), - z.object({ c: z.string() }), - ]) - ), - }); - test("formats object union schema", ({ expect }) => { - const formatted = formatZodErrorForTest(objectUnionSchema, { - key: false, - objects: [false, { a: 1 }, {}, [], { d: "" }], - }); - expect(formatted).toMatchInlineSnapshot(` - "{ - key: false, - ^ Expected string, received boolean - objects: [ - /* [0] */ false, - ^ Expected object, received boolean - ..., - /* [2] */ { - a: undefined, - ^1 Required *or* - b: undefined, - ^1 Required *or* - c: undefined, - ^1 Required - }, - /* [3] */ [], - ^ Expected object, received array - /* [4] */ { - ..., - a: undefined, - ^2 Required *or* - b: undefined, - ^2 Required *or* - c: undefined, - ^2 Required - }, - ], - }" - `); - }); - test("formats object union schema in colour", ({ expect }) => { - const formatted = formatZodErrorForTest( - objectUnionSchema, - { - key: false, - objects: [false, {}, {}, {}, {}, {}, /* cycle */ {}, {}], - }, - /* colour */ true - ); - expect(formatted).toMatchInlineSnapshot(` - "{ - key: false, -  ^ Expected string, received boolean - objects: [ - /* [0] */ false, -  ^ Expected object, received boolean - /* [1] */ { - a: undefined, -  ^1 Required *or* - b: undefined, -  ^1 Required *or* - c: undefined, -  ^1 Required - }, - /* [2] */ { - a: undefined, -  ^2 Required *or* - b: undefined, -  ^2 Required *or* - c: undefined, -  ^2 Required - }, - /* [3] */ { - a: undefined, -  ^3 Required *or* - b: undefined, -  ^3 Required *or* - c: undefined, -  ^3 Required - }, - /* [4] */ { - a: undefined, -  ^4 Required *or* - b: undefined, -  ^4 Required *or* - c: undefined, -  ^4 Required - }, - /* [5] */ { - a: undefined, -  ^5 Required *or* - b: undefined, -  ^5 Required *or* - c: undefined, -  ^5 Required - }, - /* [6] */ { - a: undefined, -  ^6 Required *or* - b: undefined, -  ^6 Required *or* - c: undefined, -  ^6 Required - }, - /* [7] */ { - a: undefined, -  ^7 Required *or* - b: undefined, -  ^7 Required *or* - c: undefined, -  ^7 Required - }, - ], - }" - `); - }); - - test("formats tuple union schema", ({ expect }) => { - const formatted = formatZodErrorForTest( - z.object({ - tuples: z.array( - z.union([ - z.tuple([z.string(), z.number()]), - z.tuple([z.boolean(), z.boolean(), z.boolean()]), - ]) - ), - }), - { - tuples: [false, { a: 1 }, [], ["2", "3"], [4, 5, 6], [true, 7, false]], - } - ); - expect(formatted).toMatchInlineSnapshot(` - "{ - tuples: [ - /* [0] */ false, - ^ Expected array, received boolean - /* [1] */ { a: 1 }, - ^ Expected array, received object - /* [2] */ [], - ^ Array must contain at least 2 element(s) - Array must contain at least 3 element(s) - /* [3] */ [ - ..., - /* [1] */ '3', - ^ Expected number, received string - ], - /* [4] */ [ - /* [0] */ 4, - ^1 Expected string, received number - Expected boolean, received number *or* - /* [1] */ 5, - ^1 Expected boolean, received number *or* - /* [2] */ 6, - ^1 Expected boolean, received number - ], - /* [5] */ [ - /* [0] */ true, - ^2 Expected string, received boolean *or* - /* [1] */ 7, - ^2 Expected boolean, received number - ..., - ], - ], - }" - `); - }); - - test("formats custom message schema", ({ expect }) => { - const formatted = formatZodErrorForTest( - z.object({ - a: z.custom(() => false, { - message: "Custom message\nwith multiple\nlines", - }), - }), - { a: Symbol("kOoh") } - ); - expect(formatted).toMatchInlineSnapshot(` - "{ - a: Symbol(kOoh), - ^ Custom message - with multiple - lines - }" - `); - }); -}); diff --git a/packages/vite-plugin-cloudflare/playground/email-worker/__tests__/email-triggers.spec.ts b/packages/vite-plugin-cloudflare/playground/email-worker/__tests__/email-triggers.spec.ts index 613c596acf4..637ab8e5dac 100644 --- a/packages/vite-plugin-cloudflare/playground/email-worker/__tests__/email-triggers.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/email-worker/__tests__/email-triggers.spec.ts @@ -1,5 +1,5 @@ import { readFileSync } from "node:fs"; -import path from "node:path"; +import { join } from "node:path"; import dedent from "ts-dedent"; import { describe, test, vi } from "vitest"; import { @@ -72,9 +72,7 @@ test("logs sent emails to a directory within the project directory", async ({ return emailPath as string; }, WAIT_FOR_OPTIONS); - const projectEmailDir = slash( - path.join(rootDir, ".wrangler", "tmp", "email") - ); + const projectEmailDir = slash(join(rootDir, ".wrangler", "tmp", "email")); expect(slash(loggedPath).startsWith(projectEmailDir)).toBe(true); const fileContents = readFileSync(loggedPath, "utf-8"); diff --git a/packages/vitest-pool-workers/package.json b/packages/vitest-pool-workers/package.json index 20b5c2aa6fe..2d60c593c1b 100644 --- a/packages/vitest-pool-workers/package.json +++ b/packages/vitest-pool-workers/package.json @@ -57,7 +57,7 @@ "esbuild": "catalog:default", "miniflare": "workspace:*", "wrangler": "workspace:*", - "zod": "3.25.76" + "zod": "catalog:default" }, "devDependencies": { "@cloudflare/mock-npm-registry": "workspace:*", diff --git a/packages/vitest-pool-workers/src/pool/config.ts b/packages/vitest-pool-workers/src/pool/config.ts index a6ee42ca974..a16cd6fdfbf 100644 --- a/packages/vitest-pool-workers/src/pool/config.ts +++ b/packages/vitest-pool-workers/src/pool/config.ts @@ -1,6 +1,6 @@ import path from "node:path"; +import { formatZodError } from "@cloudflare/workers-utils"; import { - formatZodError, getRootPath, Log, LogLevel, @@ -17,7 +17,7 @@ import { import type { ModuleRule, WorkerOptions } from "miniflare"; import type { TestProject } from "vitest/node"; import type { Binding, RemoteProxySession } from "wrangler"; -import type { ParseParams, ZodError } from "zod"; +import type { ZodError } from "zod"; export interface WorkersConfigPluginAPI { setMain(newMain?: string): void; @@ -35,7 +35,7 @@ const WorkersPoolOptionsSchema = z.object({ * `module` instance as is used internally for the `SELF` and Durable Object * bindings. */ - main: z.ostring(), + main: z.string().optional(), /** * Enables remote bindings to access remote resources configured * with `remote: true` in the wrangler configuration file. @@ -60,13 +60,15 @@ const WorkersPoolOptionsSchema = z.object({ ) .default({}), miniflare: z - .object({ - workers: z.array(z.object({}).passthrough()).optional(), + .looseObject({ + workers: z.array(z.looseObject({})).optional(), }) - .passthrough() .optional(), wrangler: z - .object({ configPath: z.ostring(), environment: z.ostring() }) + .object({ + configPath: z.string().optional(), + environment: z.string().optional(), + }) .optional(), }); @@ -89,7 +91,7 @@ export type WorkersPoolOptionsWithDefines = WorkersPoolOptions & { defines?: Record; }; -type PathParseParams = Pick; +type PathParseParams = { path?: (string | number)[] }; function isZodErrorLike(value: unknown): value is ZodError { return ( diff --git a/packages/vitest-pool-workers/test/validation.test.ts b/packages/vitest-pool-workers/test/validation.test.ts index f43fdea5e7c..203d3fc63f1 100644 --- a/packages/vitest-pool-workers/test/validation.test.ts +++ b/packages/vitest-pool-workers/test/validation.test.ts @@ -23,9 +23,9 @@ test( TypeError: Unexpected options in project ${path.join(tmpPathName, "vitest.config.mts")}: { miniflare: [], - ^ Expected object, received array + ^ Invalid input: expected object, received array wrangler: './wrangler.toml', - ^ Expected object, received string + ^ Invalid input: expected object, received string } `; expect(result.stderr).toMatch(expected); @@ -46,7 +46,7 @@ test( { miniflare: { compatibilityDate: { year: 2024, month: 1, day: 1 }, - ^ Expected string, received object + ^ Invalid input: expected string, received object }, } `; diff --git a/packages/workers-shared/package.json b/packages/workers-shared/package.json index 0f1e837fdc0..388ea7423bb 100644 --- a/packages/workers-shared/package.json +++ b/packages/workers-shared/package.json @@ -52,7 +52,7 @@ "toucan-js": "4.0.0", "typescript": "catalog:default", "vitest": "catalog:default", - "zod": "^3.25.76" + "zod": "catalog:default" }, "engines": { "node": ">=22.0.0" diff --git a/packages/workers-shared/utils/types.ts b/packages/workers-shared/utils/types.ts index 1cd4374b3c1..acb70865e03 100644 --- a/packages/workers-shared/utils/types.ts +++ b/packages/workers-shared/utils/types.ts @@ -38,17 +38,20 @@ const MetadataRedirectEntry = z.object({ to: z.string(), }); -const MetadataStaticRedirects = z.record(MetadataStaticRedirectEntry); +const MetadataStaticRedirects = z.record( + z.string(), + MetadataStaticRedirectEntry +); export type MetadataStaticRedirects = z.infer; -const MetadataRedirects = z.record(MetadataRedirectEntry); +const MetadataRedirects = z.record(z.string(), MetadataRedirectEntry); export type MetadataRedirects = z.infer; const MetadataHeaderEntry = z.object({ - set: z.record(z.string()).optional(), + set: z.record(z.string(), z.string()).optional(), unset: z.array(z.string()).optional(), }); -const MetadataHeaders = z.record(MetadataHeaderEntry); +const MetadataHeaders = z.record(z.string(), MetadataHeaderEntry); export type MetadataHeaders = z.infer; export const RedirectsSchema = z diff --git a/packages/workers-utils/package.json b/packages/workers-utils/package.json index d012ef4548b..affa631b333 100644 --- a/packages/workers-utils/package.json +++ b/packages/workers-utils/package.json @@ -65,6 +65,7 @@ "concurrently": "^8.2.2", "empathic": "^2.0.0", "jsonc-parser": "catalog:default", + "kleur": "^4.1.5", "open": "catalog:default", "signal-exit": "catalog:default", "smol-toml": "catalog:default", @@ -74,7 +75,8 @@ "typescript": "catalog:default", "update-check": "^1.5.4", "vitest": "catalog:default", - "xdg-app-paths": "^8.3.0" + "xdg-app-paths": "^8.3.0", + "zod": "catalog:default" }, "peerDependencies": { "vitest": "^4.1.0" diff --git a/packages/workers-utils/src/index.ts b/packages/workers-utils/src/index.ts index 833b0cd0aef..526e0ed381e 100644 --- a/packages/workers-utils/src/index.ts +++ b/packages/workers-utils/src/index.ts @@ -171,3 +171,5 @@ export { getWorkerName, getWorkerNameFromProject, } from "./worker-name"; + +export { _forceColour, formatZodError } from "./zod-format"; diff --git a/packages/miniflare/src/zod-format.ts b/packages/workers-utils/src/zod-format.ts similarity index 83% rename from packages/miniflare/src/zod-format.ts rename to packages/workers-utils/src/zod-format.ts index 465bedce476..d50c15e41ea 100644 --- a/packages/miniflare/src/zod-format.ts +++ b/packages/workers-utils/src/zod-format.ts @@ -1,4 +1,3 @@ -// TODO(someday): publish this as a separate package // noinspection JSUnusedAssignment // ^ WebStorm incorrectly thinks some variables might not have been initialised // before use without this. TypeScript is better at catching these errors. :) @@ -17,6 +16,15 @@ import { } from "kleur/colors"; import type { z } from "zod"; +const originalEnabled = $colors.enabled; + +// `kleur` is marked as a dev dependency, so will get bundled. We'd still like +// to be able to control whether it's enabled in tests though. Therefore, export +// a function that toggles the enabled state of our bundled version. +export function _forceColour(enabled = originalEnabled) { + $colors.enabled = enabled; +} + // This file contains a `formatZodError(error, input)` function for formatting // a Zod `error` that came from parsing a specific `input`. This works by // building an "annotated" version of the `input`, with roughly the same shape, @@ -292,24 +300,38 @@ function isRecord(value: unknown): value is Record { } function arrayShallowEqual(a: T[], b: T[]) { - if (a.length !== b.length) return false; - for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return false; + } + } return true; } -function issueEqual(a: z.ZodIssue, b: z.ZodIssue) { +function issueEqual(a: z.core.$ZodIssue, b: z.core.$ZodIssue) { // We consider issues to be equal if their messages and paths are return a.message === b.message && arrayShallowEqual(a.path, b.path); } -function hasMultipleDistinctMessages(issues: z.ZodIssue[], atDepth: number) { +function hasMultipleDistinctMessages( + issues: z.core.$ZodIssue[], + atDepth: number +) { // Returns true iff `issues` has issues that aren't "the same" at the // specified depth or below - let firstIssue: z.ZodIssue | undefined; + let firstIssue: z.core.$ZodIssue | undefined; for (const issue of issues) { - if (issue.path.length < atDepth) continue; - if (firstIssue === undefined) firstIssue = issue; - else if (!issueEqual(firstIssue, issue)) return true; + if (issue.path.length < atDepth) { + continue; + } + if (firstIssue === undefined) { + firstIssue = issue; + } else if (!issueEqual(firstIssue, issue)) { + return true; + } } return false; } @@ -318,7 +340,7 @@ function annotate( groupCounts: GroupCountsMap, annotated: Annotated, input: unknown, - issue: z.ZodIssue, + issue: z.core.$ZodIssue, path: (string | number)[], groupId?: number ): Annotated { @@ -327,38 +349,52 @@ function annotate( // If this is an `invalid_union` error, make sure we include all sub-issues if (issue.code === "invalid_union") { - const unionIssues = issue.unionErrors.flatMap(({ issues }) => issues); - - // If the `input` is an object/array with multiple distinct messages, - // annotate it as a group - let newGroupId: number | undefined; - const multipleDistinct = hasMultipleDistinctMessages( - unionIssues, - // For this check, we only include messages that are deeper than our - // current level, so we don't include messages we'd ignore if we grouped - issue.path.length + 1 - ); - if (isRecord(input) && multipleDistinct) { - newGroupId = groupCounts.size; - groupCounts.set(newGroupId, 0); - } + // In zod v4, `issue.errors` is `$ZodIssue[][]` where each inner + // array holds the issues for one union branch. + const allBranchIssues = issue.errors; + const unionIssues = allBranchIssues.flat(); + + // In zod v4, discriminated unions with an invalid discriminator + // produce an `invalid_union` issue with an empty `errors` array + // and the error message on the issue itself. Fall through to + // treat it as a regular annotation in that case. + if (unionIssues.length > 0) { + // If the `input` is an object/array with multiple distinct messages, + // annotate it as a group + // In zod v4, branch issue paths are already relative to the union + // issue, so use depth 1 (deeper than current level 0) rather than + // issue.path.length + 1. + const multipleDistinct = hasMultipleDistinctMessages(unionIssues, 1); - for (const unionIssue of unionIssues) { - const unionPath = unionIssue.path.slice(issue.path.length); - // If we have multiple distinct messages at deeper levels, and this - // issue is for the current path, skip it, so we don't end up annotating - // the current path and sub-paths - if (multipleDistinct && unionPath.length === 0) continue; - annotated = annotate( - groupCounts, - annotated, - input, - unionIssue, - unionPath, - newGroupId - ); + let newGroupId: number | undefined; + if (isRecord(input) && multipleDistinct) { + newGroupId = groupCounts.size; + groupCounts.set(newGroupId, 0); + } + + for (const branchIssues of allBranchIssues) { + for (const unionIssue of branchIssues) { + // In zod v4, branch issue paths are already relative to the + // union issue (not absolute), so use them directly. + const unionPath = unionIssue.path as (string | number)[]; + // If we have multiple distinct messages at deeper levels, and this + // issue is for the current path, skip it, so we don't end up annotating + // the current path and sub-paths + if (multipleDistinct && unionPath.length === 0) { + continue; + } + annotated = annotate( + groupCounts, + annotated, + input, + unionIssue, + unionPath, + newGroupId + ); + } + } + return annotated; } - return annotated; } const message = issue.message; @@ -449,7 +485,9 @@ function print( messagePrefix += annotated[kGroupId] + 1; const remaining = groupCounts.get(annotated[kGroupId]); assert(remaining !== undefined); - if (remaining > 1) groupOr = " *or*"; + if (remaining > 1) { + groupOr = " *or*"; + } groupCounts.set(annotated[kGroupId], remaining - 1); } messagePrefix += " "; @@ -514,8 +552,12 @@ export function formatZodError(error: z.ZodError, input: unknown): string { // annotate the input with an `invalid_type` error instead const sortedIssues = Array.from(error.issues).sort((a, b) => { if (a.code !== b.code) { - if (a.code === "invalid_union") return -1; - if (b.code === "invalid_union") return 1; + if (a.code === "invalid_union") { + return -1; + } + if (b.code === "invalid_union") { + return 1; + } } return 0; }); @@ -524,7 +566,13 @@ export function formatZodError(error: z.ZodError, input: unknown): string { let annotated: Annotated; const groupCounts = new GroupCountsMap(); for (const issue of sortedIssues) { - annotated = annotate(groupCounts, annotated, input, issue, issue.path); + annotated = annotate( + groupCounts, + annotated, + input, + issue, + issue.path as (string | number)[] + ); } // Print to pretty string diff --git a/packages/workers-utils/tests/zod-format.test.ts b/packages/workers-utils/tests/zod-format.test.ts new file mode 100644 index 00000000000..02a15604253 --- /dev/null +++ b/packages/workers-utils/tests/zod-format.test.ts @@ -0,0 +1,458 @@ +import assert from "node:assert"; +import { _forceColour, formatZodError } from "@cloudflare/workers-utils"; +import { describe, test } from "vitest"; +import { z } from "zod"; + +function formatZodErrorForTest( + schema: z.ZodType, + input: unknown, + colour?: boolean +) { + const result = schema.safeParse(input); + assert(!result.success); + // Disable colours by default for easier-to-read snapshots + _forceColour(colour ?? false); + return formatZodError(result.error, input); +} + +describe("formatZodError:", () => { + test("formats primitive schema with primitive input", ({ expect }) => { + const formatted = formatZodErrorForTest(z.number(), false); + expect(formatted).toMatchInlineSnapshot(` + "false + ^ Invalid input: expected number, received boolean" + `); + }); + test("formats primitive schema with object input", ({ expect }) => { + const formatted = formatZodErrorForTest(z.string(), { + a: 1, + b: { c: 1 }, + }); + expect(formatted).toMatchInlineSnapshot(` + "{ a: 1, b: [Object] } + ^ Invalid input: expected string, received object" + `); + }); + + test("formats object schema with primitive input", ({ expect }) => { + const formatted = formatZodErrorForTest(z.object({ a: z.number() }), true); + expect(formatted).toMatchInlineSnapshot(` + "true + ^ Invalid input: expected object, received boolean" + `); + }); + test("formats object schema with object input", ({ expect }) => { + const formatted = formatZodErrorForTest( + z.object({ + a: z.string(), + b: z.number(), + c: z.boolean(), + d: z.number(), + e: z.number(), + f: z.boolean(), + g: z.boolean(), + }), + { + a: "", // Check skips valid + b: "2", + c: true, // Check skips valid + d: 4, // Check doesn't duplicate `...` when skipping valid + e: 5, + /*f*/ // Check required options + g: "7", + } + ); + expect(formatted).toMatchInlineSnapshot(` + "{ + ..., + b: '2', + ^ Invalid input: expected number, received string + ..., + g: '7', + ^ Invalid input: expected boolean, received string + f: undefined, + ^ Invalid input: expected boolean, received undefined + }" + `); + }); + test("formats object schema with additional options", ({ expect }) => { + const formatted = formatZodErrorForTest( + z.object({ a: z.number() }).strict(), + { a: 1, b: 2 } + ); + expect(formatted).toMatchInlineSnapshot(` + "{ a: 1, b: 2 } + ^ Unrecognized key: "b"" + `); + }); + + test("formats array schema with primitive input", ({ expect }) => { + const formatted = formatZodErrorForTest(z.array(z.boolean()), 1); + expect(formatted).toMatchInlineSnapshot(` + "1 + ^ Invalid input: expected array, received number" + `); + }); + test("formats array schema with array input", ({ expect }) => { + const formatted = formatZodErrorForTest(z.array(z.number()), [ + 1, // Check skips valid + 2, // Check doesn't duplicate `...` when skipping valid + "3", + 4, + 5, + false, + ]); + expect(formatted).toMatchInlineSnapshot(` + "[ + ..., + /* [2] */ '3', + ^ Invalid input: expected number, received string + ..., + /* [5] */ false, + ^ Invalid input: expected number, received boolean + ]" + `); + }); + test("formats array schema with additional options", ({ expect }) => { + const formatted = formatZodErrorForTest( + z.array(z.number()).max(3), + [1, 2, 3, 4, 5] + ); + expect(formatted).toMatchInlineSnapshot(` + "[ 1, 2, 3, 4, 5 ] + ^ Too big: expected array to have <=3 items" + `); + }); + + test("formats deeply nested schema", ({ expect }) => { + const formatted = formatZodErrorForTest( + z.object({ + a: z.number(), + b: z.object({ + c: z.string(), + d: z.array(z.object({ e: z.boolean() })), + f: z.array(z.number()), + }), + g: z.string(), + }), + { + a: "1", + b: { + c: 2, + d: [{ e: true }, { e: 42 }, false, {}], + f: () => {}, + }, + } + ); + expect(formatted).toMatchInlineSnapshot(` + "{ + a: '1', + ^ Invalid input: expected number, received string + b: { + c: 2, + ^ Invalid input: expected string, received number + d: [ + ..., + /* [1] */ { + e: 42, + ^ Invalid input: expected boolean, received number + }, + /* [2] */ false, + ^ Invalid input: expected object, received boolean + /* [3] */ { + e: undefined, + ^ Invalid input: expected boolean, received undefined + }, + ], + f: [Function: f], + ^ Invalid input: expected array, received function + }, + g: undefined, + ^ Invalid input: expected string, received undefined + }" + `); + }); + + test("formats large actual values", ({ expect }) => { + const formatted = formatZodErrorForTest( + z.object({ + a: z.object({ + b: z.string(), + }), + }), + { + a: { + // Check indents inspected value at correct depth + b: Array.from({ length: 50 }).map((_, i) => i), + }, + } + ); + expect(formatted).toMatchInlineSnapshot(` + "{ + a: { + b: [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, + 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, + 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, + 44, 45, 46, 47, 48, 49 + ], + ^ Invalid input: expected string, received array + }, + }" + `); + }); + + test("formats union schema", ({ expect }) => { + const formatted = formatZodErrorForTest( + z.union([z.boolean(), z.literal(1)]), + "a" + ); + expect(formatted).toMatchInlineSnapshot(` + "'a' + ^ Invalid input: expected boolean, received string + Invalid input: expected 1" + `); + }); + + const discriminatedUnionSchema = z.discriminatedUnion("type", [ + z.object({ + type: z.literal("a"), + a: z.number(), + }), + z.object({ + type: z.literal("b"), + b: z.boolean(), + }), + ]); + test("formats discriminated union schema", ({ expect }) => { + const formatted = formatZodErrorForTest(discriminatedUnionSchema, { + type: "a", + a: false, + }); + expect(formatted).toMatchInlineSnapshot(` + "{ + ..., + a: false, + ^ Invalid input: expected number, received boolean + }" + `); + }); + test("formats discriminated union schema with invalid discriminator", ({ + expect, + }) => { + const formatted = formatZodErrorForTest(discriminatedUnionSchema, { + type: "c", + }); + expect(formatted).toMatchInlineSnapshot(` + "{ + type: 'c', + ^ Invalid discriminator value. Expected 'a' | 'b' + }" + `); + }); + + test("formats intersection schema", ({ expect }) => { + const formatted = formatZodErrorForTest( + z.intersection(z.number(), z.literal(2)), + false + ); + expect(formatted).toMatchInlineSnapshot(` + "false + ^ Invalid input: expected number, received boolean + Invalid input: expected 2" + `); + }); + + const objectUnionSchema = z.object({ + key: z.string(), + objects: z.array( + z.union([ + z.object({ a: z.number() }), + z.object({ b: z.boolean() }), + z.object({ c: z.string() }), + ]) + ), + }); + test("formats object union schema", ({ expect }) => { + const formatted = formatZodErrorForTest(objectUnionSchema, { + key: false, + objects: [false, { a: 1 }, {}, [], { d: "" }], + }); + expect(formatted).toMatchInlineSnapshot(` + "{ + key: false, + ^ Invalid input: expected string, received boolean + objects: [ + /* [0] */ false, + ^ Invalid input: expected object, received boolean + ..., + /* [2] */ { + a: undefined, + ^1 Invalid input: expected number, received undefined *or* + b: undefined, + ^1 Invalid input: expected boolean, received undefined *or* + c: undefined, + ^1 Invalid input: expected string, received undefined + }, + /* [3] */ [], + ^ Invalid input: expected object, received array + /* [4] */ { + ..., + a: undefined, + ^2 Invalid input: expected number, received undefined *or* + b: undefined, + ^2 Invalid input: expected boolean, received undefined *or* + c: undefined, + ^2 Invalid input: expected string, received undefined + }, + ], + }" + `); + }); + test("formats object union schema in colour", ({ expect }) => { + const formatted = formatZodErrorForTest( + objectUnionSchema, + { + key: false, + objects: [false, {}, {}, {}, {}, {}, /* cycle */ {}, {}], + }, + /* colour */ true + ); + expect(formatted).toMatchInlineSnapshot(` + "{ + key: false, +  ^ Invalid input: expected string, received boolean + objects: [ + /* [0] */ false, +  ^ Invalid input: expected object, received boolean + /* [1] */ { + a: undefined, +  ^1 Invalid input: expected number, received undefined *or* + b: undefined, +  ^1 Invalid input: expected boolean, received undefined *or* + c: undefined, +  ^1 Invalid input: expected string, received undefined + }, + /* [2] */ { + a: undefined, +  ^2 Invalid input: expected number, received undefined *or* + b: undefined, +  ^2 Invalid input: expected boolean, received undefined *or* + c: undefined, +  ^2 Invalid input: expected string, received undefined + }, + /* [3] */ { + a: undefined, +  ^3 Invalid input: expected number, received undefined *or* + b: undefined, +  ^3 Invalid input: expected boolean, received undefined *or* + c: undefined, +  ^3 Invalid input: expected string, received undefined + }, + /* [4] */ { + a: undefined, +  ^4 Invalid input: expected number, received undefined *or* + b: undefined, +  ^4 Invalid input: expected boolean, received undefined *or* + c: undefined, +  ^4 Invalid input: expected string, received undefined + }, + /* [5] */ { + a: undefined, +  ^5 Invalid input: expected number, received undefined *or* + b: undefined, +  ^5 Invalid input: expected boolean, received undefined *or* + c: undefined, +  ^5 Invalid input: expected string, received undefined + }, + /* [6] */ { + a: undefined, +  ^6 Invalid input: expected number, received undefined *or* + b: undefined, +  ^6 Invalid input: expected boolean, received undefined *or* + c: undefined, +  ^6 Invalid input: expected string, received undefined + }, + /* [7] */ { + a: undefined, +  ^7 Invalid input: expected number, received undefined *or* + b: undefined, +  ^7 Invalid input: expected boolean, received undefined *or* + c: undefined, +  ^7 Invalid input: expected string, received undefined + }, + ], + }" + `); + }); + + test("formats tuple union schema", ({ expect }) => { + const formatted = formatZodErrorForTest( + z.object({ + tuples: z.array( + z.union([ + z.tuple([z.string(), z.number()]), + z.tuple([z.boolean(), z.boolean(), z.boolean()]), + ]) + ), + }), + { + tuples: [false, { a: 1 }, [], ["2", "3"], [4, 5, 6], [true, 7, false]], + } + ); + expect(formatted).toMatchInlineSnapshot(` + "{ + tuples: [ + /* [0] */ false, + ^ Invalid input: expected tuple, received boolean + /* [1] */ { a: 1 }, + ^ Invalid input: expected tuple, received object + /* [2] */ [], + ^ Too small: expected array to have >=2 items + Too small: expected array to have >=3 items + /* [3] */ [ + ..., + /* [1] */ '3', + ^ Invalid input: expected number, received string + ], + /* [4] */ [ + /* [0] */ 4, + ^1 Invalid input: expected string, received number + Invalid input: expected boolean, received number *or* + /* [1] */ 5, + ^1 Invalid input: expected boolean, received number *or* + /* [2] */ 6, + ^1 Invalid input: expected boolean, received number + ], + /* [5] */ [ + /* [0] */ true, + ^2 Invalid input: expected string, received boolean *or* + /* [1] */ 7, + ^2 Invalid input: expected boolean, received number + ..., + ], + ], + }" + `); + }); + + test("formats custom message schema", ({ expect }) => { + const formatted = formatZodErrorForTest( + z.object({ + a: z.custom(() => false, { + message: "Custom message\nwith multiple\nlines", + }), + }), + { a: Symbol("kOoh") } + ); + expect(formatted).toMatchInlineSnapshot(` + "{ + a: Symbol(kOoh), + ^ Custom message + with multiple + lines + }" + `); + }); +}); diff --git a/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts b/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts index 1ca8cae9c9c..a38e11118b5 100644 --- a/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts +++ b/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts @@ -407,6 +407,7 @@ export class LocalRuntimeController extends RuntimeController { }); } ); + options.handleUncaughtError = this.dispatchRuntimeError; // Bail out if a newer bundle arrived while we were building // miniflare options — avoid a redundant local server reload. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9a05df14f8..54f5dc8017a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,7 +11,7 @@ catalogs: version: 0.13.3 '@cloudflare/workers-types': specifier: ^5.20260714.1 - version: 5.20260714.1 + version: 5.20260716.1 '@hey-api/openapi-ts': specifier: 0.94.0 version: 0.94.0 @@ -191,7 +191,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@fixture/shared': specifier: workspace:* version: link:../shared @@ -239,7 +239,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@types/node': specifier: 22.15.17 version: 22.15.17 @@ -266,7 +266,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@playwright/test': specifier: catalog:default version: 1.60.0 @@ -296,7 +296,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -317,7 +317,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -341,7 +341,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -377,7 +377,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 vitest: specifier: catalog:default version: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.9.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) @@ -392,7 +392,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 undici: specifier: catalog:default version: 7.28.0 @@ -407,7 +407,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@types/mimetext': specifier: ^2.0.4 version: 2.0.4 @@ -446,7 +446,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@fixture/shared': specifier: workspace:* version: link:../shared @@ -470,7 +470,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@types/jest-image-snapshot': specifier: ^6.4.0 version: 6.4.0 @@ -497,7 +497,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 miniflare: specifier: workspace:* version: link:../../packages/miniflare @@ -567,7 +567,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@types/is-even': specifier: ^1.0.2 version: 1.0.2 @@ -585,11 +585,11 @@ importers: dependencies: '@sentry/cloudflare': specifier: ^10 - version: 10.50.0(@cloudflare/workers-types@5.20260714.1) + version: 10.50.0(@cloudflare/workers-types@5.20260716.1) devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 vitest: specifier: catalog:default version: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.9.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) @@ -620,7 +620,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -648,7 +648,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@types/node': specifier: 22.15.17 version: 22.15.17 @@ -678,7 +678,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 undici: specifier: catalog:default version: 7.28.0 @@ -696,7 +696,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@types/debug': specifier: 4.1.12 version: 4.1.12 @@ -729,7 +729,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -754,7 +754,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@fixture/pages-plugin': specifier: workspace:* version: link:../pages-plugin-example @@ -778,7 +778,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -817,7 +817,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -838,7 +838,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -859,7 +859,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -877,7 +877,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 is-odd: specifier: ^3.0.1 version: 3.0.1 @@ -896,7 +896,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@fixture/pages-plugin': specifier: workspace:* version: link:../pages-plugin-example @@ -956,7 +956,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -977,7 +977,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1133,19 +1133,19 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 fixtures/rules-app: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 fixtures/secrets-store: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 wrangler: specifier: workspace:* version: link:../../packages/wrangler @@ -1172,7 +1172,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@types/is-even': specifier: ^1.0.2 version: 1.0.2 @@ -1196,7 +1196,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 vitest: specifier: catalog:default version: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.9.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) @@ -1211,7 +1211,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 esbuild: specifier: catalog:default version: 0.28.1 @@ -1229,7 +1229,7 @@ importers: devDependencies: '@better-auth/stripe': specifier: ^1.4.6 - version: 1.5.4(f660eebfb07999a7aa04507f74bf93ad) + version: 1.5.4(eef0467959f7f6b3622637d2501852d3) '@cloudflare/containers': specifier: ^0.2.2 version: 0.2.2 @@ -1238,7 +1238,7 @@ importers: version: link:../../packages/vitest-pool-workers '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@microlabs/otel-cf-workers': specifier: 1.0.0-rc.45 version: 1.0.0-rc.45(@opentelemetry/api@1.9.1) @@ -1253,7 +1253,7 @@ importers: version: 3.2.6 better-auth: specifier: ^1.4.6 - version: 1.5.4(@cloudflare/workers-types@5.20260714.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@5.20260714.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0) + version: 1.5.4(@cloudflare/workers-types@5.20260716.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@5.20260716.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0) cjs-wasm-module-dep: specifier: file:./module-resolution/vendor/cjs-wasm-module-dep version: file:fixtures/vitest-pool-workers-examples/module-resolution/vendor/cjs-wasm-module-dep @@ -1327,7 +1327,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@fixture/shared': specifier: workspace:* version: link:../shared @@ -1382,7 +1382,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 wrangler: specifier: workspace:* version: link:../../packages/wrangler @@ -1394,7 +1394,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 miniflare: specifier: workspace:* version: link:../../packages/miniflare @@ -1442,7 +1442,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 run-script-os: specifier: ^1.1.6 version: 1.1.6 @@ -1466,7 +1466,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1487,7 +1487,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1508,7 +1508,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1529,7 +1529,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1550,7 +1550,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@types/jest-image-snapshot': specifier: ^6.4.0 version: 6.4.0 @@ -1583,7 +1583,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@types/node': specifier: 22.15.17 version: 22.15.17 @@ -1610,7 +1610,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1628,7 +1628,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1692,7 +1692,7 @@ importers: version: 5.8.3 vitest: specifier: catalog:default - version: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.8.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) + version: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.8.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.23.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) packages/chrome-devtools-patches: devDependencies: @@ -1762,7 +1762,7 @@ importers: version: 5.8.3 vitest: specifier: catalog:default - version: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.8.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.23.1)(jiti@2.6.1)(tsx@3.12.10)(yaml@2.8.1)) + version: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.8.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@3.12.10)(yaml@2.8.1)) packages/config: dependencies: @@ -1775,7 +1775,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -1838,7 +1838,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -2046,7 +2046,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@octokit/types': specifier: ^13.8.0 version: 13.8.0 @@ -2067,7 +2067,7 @@ importers: version: link:../vitest-pool-workers '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -2091,7 +2091,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -2118,10 +2118,10 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: catalog:default - version: 0.13.3(@cloudflare/workers-types@5.20260714.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) + version: 0.13.3(@cloudflare/workers-types@5.20260716.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@types/mime': specifier: ^3.0.4 version: 3.0.4 @@ -2297,7 +2297,7 @@ importers: version: link:../workers-shared '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -2422,8 +2422,8 @@ importers: specifier: ^2.0.2 version: 2.0.2 zod: - specifier: ^3.25.76 - version: 3.25.76 + specifier: catalog:default + version: 4.4.3 packages/mock-npm-registry: devDependencies: @@ -2460,7 +2460,7 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: catalog:default - version: 0.13.3(@cloudflare/workers-types@5.20260714.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) + version: 0.13.3(@cloudflare/workers-types@5.20260716.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) '@cloudflare/workers-shared': specifier: workspace:* version: link:../workers-shared @@ -2469,7 +2469,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -2497,7 +2497,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -2531,7 +2531,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@types/node': specifier: 22.15.17 version: 22.15.17 @@ -2555,7 +2555,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 esbuild: specifier: catalog:default version: 0.28.1 @@ -2672,7 +2672,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -2768,7 +2768,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2789,7 +2789,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2810,7 +2810,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2831,7 +2831,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2852,7 +2852,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2873,7 +2873,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2894,7 +2894,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2915,7 +2915,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2936,7 +2936,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2957,7 +2957,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2978,7 +2978,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2999,7 +2999,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3020,7 +3020,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3041,7 +3041,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3062,7 +3062,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3083,7 +3083,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3104,7 +3104,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@types/mimetext': specifier: ^2.0.4 version: 2.0.4 @@ -3137,7 +3137,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3158,7 +3158,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3179,7 +3179,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3200,7 +3200,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3221,7 +3221,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3242,7 +3242,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3263,7 +3263,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3284,7 +3284,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3305,7 +3305,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@playground/main-resolution-package': specifier: file:./package version: file:packages/vite-plugin-cloudflare/playground/main-resolution/package @@ -3329,7 +3329,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@types/express': specifier: ^5.0.1 version: 5.0.1 @@ -3356,7 +3356,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@playground/module-resolution-excludes': specifier: file:./packages/excludes version: file:packages/vite-plugin-cloudflare/playground/module-resolution/packages/excludes @@ -3368,7 +3368,7 @@ importers: version: file:packages/vite-plugin-cloudflare/playground/module-resolution/packages/requires '@remix-run/cloudflare': specifier: 2.12.0 - version: 2.12.0(@cloudflare/workers-types@5.20260714.1)(typescript@5.8.3) + version: 2.12.0(@cloudflare/workers-types@5.20260716.1)(typescript@5.8.3) '@types/react': specifier: ^18.3.11 version: 18.3.18 @@ -3401,7 +3401,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3422,7 +3422,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@fixture/shared': specifier: workspace:* version: link:../../../../fixtures/shared @@ -3474,7 +3474,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@types/react': specifier: 19.1.0 version: 19.1.0 @@ -3495,7 +3495,7 @@ importers: dependencies: partyserver: specifier: ^0.3.3 - version: 0.3.3(@cloudflare/workers-types@5.20260714.1) + version: 0.3.3(@cloudflare/workers-types@5.20260716.1) partysocket: specifier: ^1.1.16 version: 1.1.16(react@19.2.1) @@ -3514,7 +3514,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@tailwindcss/vite': specifier: ^4.2.1 version: 4.2.2(vite@8.0.13(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) @@ -3550,7 +3550,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3571,7 +3571,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../../../workers-utils @@ -3611,7 +3611,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@types/react': specifier: 19.1.0 version: 19.1.0 @@ -3641,7 +3641,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3662,7 +3662,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3690,7 +3690,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@types/react': specifier: 19.1.0 version: 19.1.0 @@ -3723,7 +3723,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3744,7 +3744,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3765,7 +3765,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3786,7 +3786,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@vitejs/plugin-basic-ssl': specifier: ^2.2.0 version: 2.2.0(vite@8.0.13(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) @@ -3810,7 +3810,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3831,7 +3831,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3852,7 +3852,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3873,7 +3873,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3899,8 +3899,8 @@ importers: specifier: workspace:* version: link:../wrangler zod: - specifier: 3.25.76 - version: 3.25.76 + specifier: catalog:default + version: 4.4.3 devDependencies: '@cloudflare/mock-npm-registry': specifier: workspace:* @@ -3910,7 +3910,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -4168,13 +4168,13 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: catalog:default - version: 0.13.3(@cloudflare/workers-types@5.20260714.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) + version: 0.13.3(@cloudflare/workers-types@5.20260716.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) '@cloudflare/workers-tsconfig': specifier: workspace:* version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@sentry/cli': specifier: ^2.37.0 version: 2.41.1(encoding@0.1.13) @@ -4200,8 +4200,8 @@ importers: specifier: catalog:default version: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.8.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) zod: - specifier: ^3.25.76 - version: 3.25.76 + specifier: catalog:default + version: 4.4.3 packages/workers-tsconfig: {} @@ -4247,6 +4247,9 @@ importers: jsonc-parser: specifier: catalog:default version: 3.2.0 + kleur: + specifier: ^4.1.5 + version: 4.1.5 open: specifier: catalog:default version: 11.0.0 @@ -4277,6 +4280,9 @@ importers: xdg-app-paths: specifier: ^8.3.0 version: 8.3.0 + zod: + specifier: catalog:default + version: 4.4.3 packages/workflows-shared: dependencies: @@ -4295,13 +4301,13 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: catalog:default - version: 0.13.3(@cloudflare/workers-types@5.20260714.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) + version: 0.13.3(@cloudflare/workers-types@5.20260716.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) '@cloudflare/workers-tsconfig': specifier: workspace:* version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@types/mime': specifier: ^3.0.4 version: 3.0.4 @@ -4386,7 +4392,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 5.20260714.1 + version: 5.20260716.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -5641,8 +5647,8 @@ packages: '@cloudflare/workers-types@4.20260702.1': resolution: {integrity: sha512-mOhf5TUEB1m2vPrxtqoIGfz0fUC9xyxRDx5gWHy5s+OCo6dcV+g7wI1R7gYCMFohhqF/2y2xeKVwMwCJjfn/WA==} - '@cloudflare/workers-types@5.20260714.1': - resolution: {integrity: sha512-HGCTQVIQwzqAMLrZBCgLDrWKMgOdi6yn+S0Do1rF6z7t8tVvNprQfa53V6EDRRwsVO92uN5gviX2SxASDINZfA==} + '@cloudflare/workers-types@5.20260716.1': + resolution: {integrity: sha512-LqQPmGAvdpQxzZGAMlDI6fnCsTlr8nRQibWsREaERqD0PucwFl25aXpUskSob70uSY2n6K1sp6Te9xkmzSFgaw==} '@codemirror/autocomplete@6.20.0': resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==} @@ -5724,15 +5730,15 @@ packages: '@esbuild-kit/cjs-loader@2.4.4': resolution: {integrity: sha512-NfsJX4PdzhwSkfJukczyUiZGc7zNNWZcEAyqeISpDnn0PTfzMJR1aR8xAIPskBejIxBJbIgCCMzbaYa9SXepIg==} - deprecated: 'Merged into tsx: https://tsx.hirok.io' + deprecated: 'Merged into tsx: https://tsx.is' '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} - deprecated: 'Merged into tsx: https://tsx.hirok.io' + deprecated: 'Merged into tsx: https://tsx.is' '@esbuild-kit/esm-loader@2.6.5': resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} - deprecated: 'Merged into tsx: https://tsx.hirok.io' + deprecated: 'Merged into tsx: https://tsx.is' '@esbuild/aix-ppc64@0.23.1': resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} @@ -16539,7 +16545,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.13 - '@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1)': + '@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1)': dependencies: '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 @@ -16550,9 +16556,9 @@ snapshots: nanostores: 1.1.1 zod: 4.4.3 optionalDependencies: - '@cloudflare/workers-types': 5.20260714.1 + '@cloudflare/workers-types': 5.20260716.1 - '@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1)': + '@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1)': dependencies: '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 @@ -16563,50 +16569,50 @@ snapshots: nanostores: 1.1.1 zod: 4.4.3 optionalDependencies: - '@cloudflare/workers-types': 5.20260714.1 + '@cloudflare/workers-types': 5.20260716.1 - '@better-auth/drizzle-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@cloudflare/workers-types@5.20260714.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))': + '@better-auth/drizzle-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@cloudflare/workers-types@5.20260716.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 - drizzle-orm: 0.45.1(@cloudflare/workers-types@5.20260714.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) + drizzle-orm: 0.45.1(@cloudflare/workers-types@5.20260716.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) - '@better-auth/kysely-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11)': + '@better-auth/kysely-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11)': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 kysely: 0.28.11 - '@better-auth/memory-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)': + '@better-auth/memory-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 - '@better-auth/mongo-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0)': + '@better-auth/mongo-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0)': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 mongodb: 7.1.0 - '@better-auth/prisma-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))': + '@better-auth/prisma-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 '@prisma/client': 7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3) prisma: 7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3) - '@better-auth/stripe@1.5.4(f660eebfb07999a7aa04507f74bf93ad)': + '@better-auth/stripe@1.5.4(eef0467959f7f6b3622637d2501852d3)': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) - better-auth: 1.5.4(@cloudflare/workers-types@5.20260714.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@5.20260714.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + better-auth: 1.5.4(@cloudflare/workers-types@5.20260716.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@5.20260716.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0) better-call: 1.3.2(zod@4.4.3) defu: 6.1.4 stripe: 20.4.1(@types/node@22.15.17) zod: 4.4.3 - '@better-auth/telemetry@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))': + '@better-auth/telemetry@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 @@ -17171,7 +17177,7 @@ snapshots: lodash.memoize: 4.1.2 marked: 0.3.19 - '@cloudflare/vitest-pool-workers@0.13.3(@cloudflare/workers-types@5.20260714.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0)': + '@cloudflare/vitest-pool-workers@0.13.3(@cloudflare/workers-types@5.20260716.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0)': dependencies: '@vitest/runner': 4.1.0 '@vitest/snapshot': 4.1.0 @@ -17179,7 +17185,7 @@ snapshots: esbuild: 0.27.3 miniflare: 4.20260317.1 vitest: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.9.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) - wrangler: 4.76.0(@cloudflare/workers-types@5.20260714.1) + wrangler: 4.76.0(@cloudflare/workers-types@5.20260716.1) zod: 3.25.76 transitivePeerDependencies: - '@cloudflare/workers-types' @@ -17241,7 +17247,7 @@ snapshots: '@cloudflare/workers-types@4.20260702.1': {} - '@cloudflare/workers-types@5.20260714.1': {} + '@cloudflare/workers-types@5.20260716.1': {} '@codemirror/autocomplete@6.20.0': dependencies: @@ -18937,10 +18943,10 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 - '@remix-run/cloudflare@2.12.0(@cloudflare/workers-types@5.20260714.1)(typescript@5.8.3)': + '@remix-run/cloudflare@2.12.0(@cloudflare/workers-types@5.20260716.1)(typescript@5.8.3)': dependencies: '@cloudflare/kv-asset-handler': 0.1.3 - '@cloudflare/workers-types': 5.20260714.1 + '@cloudflare/workers-types': 5.20260716.1 '@remix-run/server-runtime': 2.12.0(typescript@5.8.3) optionalDependencies: typescript: 5.8.3 @@ -19476,12 +19482,12 @@ snapshots: - encoding - supports-color - '@sentry/cloudflare@10.50.0(@cloudflare/workers-types@5.20260714.1)': + '@sentry/cloudflare@10.50.0(@cloudflare/workers-types@5.20260716.1)': dependencies: '@opentelemetry/api': 1.9.1 '@sentry/core': 10.50.0 optionalDependencies: - '@cloudflare/workers-types': 5.20260714.1 + '@cloudflare/workers-types': 5.20260716.1 '@sentry/core@10.50.0': {} @@ -20689,14 +20695,23 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.1.0(msw@2.12.4(@types/node@22.15.17)(typescript@5.8.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.23.1)(jiti@2.6.1)(tsx@3.12.10)(yaml@2.8.1))': + '@vitest/mocker@4.1.0(msw@2.12.4(@types/node@22.15.17)(typescript@5.8.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.23.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 4.1.0 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.12.4(@types/node@22.15.17)(typescript@5.8.3) + vite: 8.0.13(@types/node@22.15.17)(esbuild@0.23.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1) + + '@vitest/mocker@4.1.0(msw@2.12.4(@types/node@22.15.17)(typescript@5.8.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@3.12.10)(yaml@2.8.1))': dependencies: '@vitest/spy': 4.1.0 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.4(@types/node@22.15.17)(typescript@5.8.3) - vite: 8.0.13(@types/node@22.15.17)(esbuild@0.23.1)(jiti@2.6.1)(tsx@3.12.10)(yaml@2.8.1) + vite: 8.0.13(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@3.12.10)(yaml@2.8.1) '@vitest/mocker@4.1.0(msw@2.12.4(@types/node@22.15.17)(typescript@5.8.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1))': dependencies: @@ -21119,15 +21134,15 @@ snapshots: before-after-hook@2.2.3: {} - better-auth@1.5.4(@cloudflare/workers-types@5.20260714.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@5.20260714.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0): + better-auth@1.5.4(@cloudflare/workers-types@5.20260716.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@5.20260716.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0): dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1) - '@better-auth/drizzle-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@cloudflare/workers-types@5.20260714.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))) - '@better-auth/kysely-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11) - '@better-auth/memory-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1) - '@better-auth/mongo-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0) - '@better-auth/prisma-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) - '@better-auth/telemetry': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260714.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1)) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/drizzle-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@cloudflare/workers-types@5.20260716.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))) + '@better-auth/kysely-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11) + '@better-auth/memory-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1) + '@better-auth/mongo-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0) + '@better-auth/prisma-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) + '@better-auth/telemetry': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@5.20260716.1)(better-call@1.3.2(zod@4.4.3))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1)) '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 '@noble/ciphers': 2.1.1 @@ -21140,7 +21155,7 @@ snapshots: zod: 4.4.3 optionalDependencies: '@prisma/client': 7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3) - drizzle-orm: 0.45.1(@cloudflare/workers-types@5.20260714.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) + drizzle-orm: 0.45.1(@cloudflare/workers-types@5.20260716.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) mongodb: 7.1.0 mysql2: 3.15.3 pg: 8.16.3 @@ -21953,9 +21968,9 @@ snapshots: dependencies: wordwrap: 1.0.0 - drizzle-orm@0.45.1(@cloudflare/workers-types@5.20260714.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)): + drizzle-orm@0.45.1(@cloudflare/workers-types@5.20260716.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)): optionalDependencies: - '@cloudflare/workers-types': 5.20260714.1 + '@cloudflare/workers-types': 5.20260716.1 '@electric-sql/pglite': 0.3.2 '@opentelemetry/api': 1.9.1 '@prisma/client': 7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3) @@ -24439,9 +24454,9 @@ snapshots: parseurl@1.3.3: {} - partyserver@0.3.3(@cloudflare/workers-types@5.20260714.1): + partyserver@0.3.3(@cloudflare/workers-types@5.20260716.1): dependencies: - '@cloudflare/workers-types': 5.20260714.1 + '@cloudflare/workers-types': 5.20260716.1 nanoid: 5.1.7 partysocket@1.1.16(react@19.2.1): @@ -27014,7 +27029,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.1 - vite@8.0.13(@types/node@22.15.17)(esbuild@0.23.1)(jiti@2.6.1)(tsx@3.12.10)(yaml@2.8.1): + vite@8.0.13(@types/node@22.15.17)(esbuild@0.23.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -27026,6 +27041,21 @@ snapshots: esbuild: 0.23.1 fsevents: 2.3.3 jiti: 2.6.1 + tsx: 4.21.0 + yaml: 2.8.1 + + vite@8.0.13(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@3.12.10)(yaml@2.8.1): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.14 + rolldown: 1.0.1 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.15.17 + esbuild: 0.28.1 + fsevents: 2.3.3 + jiti: 2.6.1 tsx: 3.12.10 yaml: 2.8.1 @@ -27050,10 +27080,39 @@ snapshots: mock-socket: 9.3.1 vitest: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.8.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) - vitest@4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.8.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.23.1)(jiti@2.6.1)(tsx@3.12.10)(yaml@2.8.1)): + vitest@4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.8.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.23.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)): + dependencies: + '@vitest/expect': 4.1.0 + '@vitest/mocker': 4.1.0(msw@2.12.4(@types/node@22.15.17)(typescript@5.8.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.23.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) + '@vitest/pretty-format': 4.1.0 + '@vitest/runner': 4.1.0 + '@vitest/snapshot': 4.1.0 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.0.3 + vite: 8.0.13(@types/node@22.15.17)(esbuild@0.23.1)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 22.15.17 + '@vitest/ui': 4.1.0(vitest@4.1.0) + transitivePeerDependencies: + - msw + + vitest@4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.8.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@3.12.10)(yaml@2.8.1)): dependencies: '@vitest/expect': 4.1.0 - '@vitest/mocker': 4.1.0(msw@2.12.4(@types/node@22.15.17)(typescript@5.8.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.23.1)(jiti@2.6.1)(tsx@3.12.10)(yaml@2.8.1)) + '@vitest/mocker': 4.1.0(msw@2.12.4(@types/node@22.15.17)(typescript@5.8.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@3.12.10)(yaml@2.8.1)) '@vitest/pretty-format': 4.1.0 '@vitest/runner': 4.1.0 '@vitest/snapshot': 4.1.0 @@ -27070,7 +27129,7 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.0.3 - vite: 8.0.13(@types/node@22.15.17)(esbuild@0.23.1)(jiti@2.6.1)(tsx@3.12.10)(yaml@2.8.1) + vite: 8.0.13(@types/node@22.15.17)(esbuild@0.28.1)(jiti@2.6.1)(tsx@3.12.10)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1 @@ -27276,7 +27335,7 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260714.1 '@cloudflare/workerd-windows-64': 1.20260714.1 - wrangler@4.76.0(@cloudflare/workers-types@5.20260714.1): + wrangler@4.76.0(@cloudflare/workers-types@5.20260716.1): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 '@cloudflare/unenv-preset': 2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260317.1) @@ -27287,7 +27346,7 @@ snapshots: unenv: 2.0.0-rc.24 workerd: 1.20260317.1 optionalDependencies: - '@cloudflare/workers-types': 5.20260714.1 + '@cloudflare/workers-types': 5.20260716.1 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil diff --git a/tools/deployments/validate-catalog-usage.ts b/tools/deployments/validate-catalog-usage.ts index 992a78d3d4d..11f7540353c 100644 --- a/tools/deployments/validate-catalog-usage.ts +++ b/tools/deployments/validate-catalog-usage.ts @@ -12,12 +12,7 @@ const ROOT = resolve(__dirname, "../.."); // Deps that are deliberately pinned outside the catalog (e.g. workerd is // bumped in coordinated PRs with its own automation). -const IGNORED_DEPS = new Set([ - "workerd", - // miniflare, vitest-pool-workers, and workers-shared still use zod v3 - // and cannot adopt the v4 catalog version yet. - "zod", -]); +const IGNORED_DEPS = new Set(["workerd"]); /** * Parses the `catalog:` block of a pnpm-workspace.yaml into a map of From 9a6604c78157ff2906d83cd026908d38a79f5834 Mon Sep 17 00:00:00 2001 From: emily-shen <69125074+emily-shen@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:50:23 +0100 Subject: [PATCH 4/6] Various miniflare v5 changes (#14765) --- .../consolidate-miniflare-persist-options.md | 16 + .changeset/drop-cache-warn-usage.md | 7 + .changeset/drop-fetch-mock.md | 5 + .changeset/drop-https-key-cert-path.md | 7 + ...untime-stdio-and-structured-logs-option.md | 9 + .changeset/drop-unsafe-sticky-blobs.md | 7 + .changeset/drop-wrapped-bindings.md | 5 + ...iniflare-container-engine-shared-option.md | 16 + .../rename-cache-option-to-cache-api.md | 7 + .changeset/vitest-pool-drop-sticky-blobs.md | 7 + ...nstable-worker-options-container-engine.md | 7 + .../src/plugins/unsafe-service.ts | 12 +- packages/miniflare/scripts/build.mjs | 1 + packages/miniflare/src/http/server.ts | 21 +- packages/miniflare/src/index.ts | 172 +---- .../src/plugins/analytics-engine/index.ts | 10 +- packages/miniflare/src/plugins/cache/index.ts | 35 +- packages/miniflare/src/plugins/core/index.ts | 513 +++++-------- packages/miniflare/src/plugins/d1/index.ts | 28 +- packages/miniflare/src/plugins/do/index.ts | 22 +- packages/miniflare/src/plugins/email/index.ts | 29 +- .../src/plugins/hello-world/index.ts | 32 +- .../miniflare/src/plugins/images/index.ts | 32 +- packages/miniflare/src/plugins/index.ts | 1 - packages/miniflare/src/plugins/kv/index.ts | 29 +- .../miniflare/src/plugins/queues/index.ts | 3 +- packages/miniflare/src/plugins/r2/index.ts | 28 +- .../miniflare/src/plugins/ratelimit/index.ts | 4 +- .../src/plugins/secret-store/index.ts | 30 +- .../miniflare/src/plugins/shared/constants.ts | 11 +- .../miniflare/src/plugins/shared/index.ts | 84 +-- .../miniflare/src/plugins/stream/index.ts | 32 +- .../miniflare/src/plugins/workflows/index.ts | 25 +- packages/miniflare/src/runtime/index.ts | 65 +- packages/miniflare/src/shared/error.ts | 3 - .../src/workers/cache/cache-entry.worker.ts | 7 +- .../src/workers/cache/cache.worker.ts | 18 +- .../miniflare/src/workers/cache/constants.ts | 8 - .../src/workers/shared/blob.worker.ts | 13 +- .../miniflare/src/workers/shared/constants.ts | 1 - .../src/workers/shared/object.worker.ts | 9 +- .../src/workers/stream/object.worker.ts | 8 +- packages/miniflare/test/dev-registry.spec.ts | 6 +- .../test/fixtures/echo-plugin/index.ts | 77 ++ .../test/fixtures/unsafe-plugin/index.ts | 4 +- packages/miniflare/test/index.spec.ts | 702 +----------------- packages/miniflare/test/logs.spec.ts | 157 +--- .../test/plugins/cache/index.spec.ts | 43 +- .../core/inspector-proxy/index.spec.ts | 10 +- .../test/plugins/core/proxy/client.spec.ts | 43 +- packages/miniflare/test/plugins/d1/suite.ts | 16 +- .../miniflare/test/plugins/do/index.spec.ts | 21 +- .../test/plugins/email/index.spec.ts | 20 +- .../test/plugins/hello-world/index.spec.ts | 1 - .../test/plugins/images/index.spec.ts | 1 - .../miniflare/test/plugins/kv/index.spec.ts | 48 +- .../local-explorer/aggregation.spec.ts | 4 +- .../test/plugins/local-explorer/do.spec.ts | 2 +- .../miniflare/test/plugins/r2/index.spec.ts | 7 +- .../test/plugins/secret-store/index.spec.ts | 1 - .../test/plugins/stream/index.spec.ts | 10 +- .../test/plugins/workflows/index.spec.ts | 13 +- .../src/miniflare-options.ts | 23 +- .../vitest-pool-workers/src/pool/index.ts | 1 - .../src/api/integrations/platform/index.ts | 15 +- .../startDevWorker/LocalRuntimeController.ts | 2 - .../src/api/startDevWorker/ProxyController.ts | 4 +- packages/wrangler/src/d1/execute.ts | 5 +- packages/wrangler/src/d1/export.ts | 5 +- packages/wrangler/src/dev/miniflare/index.ts | 18 +- packages/wrangler/src/hello-world/index.ts | 4 +- packages/wrangler/src/kv/helpers.ts | 4 +- packages/wrangler/src/r2/helpers/object.ts | 4 +- .../wrangler/src/secrets-store/commands.ts | 4 +- 74 files changed, 630 insertions(+), 2024 deletions(-) create mode 100644 .changeset/consolidate-miniflare-persist-options.md create mode 100644 .changeset/drop-cache-warn-usage.md create mode 100644 .changeset/drop-fetch-mock.md create mode 100644 .changeset/drop-https-key-cert-path.md create mode 100644 .changeset/drop-runtime-stdio-and-structured-logs-option.md create mode 100644 .changeset/drop-unsafe-sticky-blobs.md create mode 100644 .changeset/drop-wrapped-bindings.md create mode 100644 .changeset/miniflare-container-engine-shared-option.md create mode 100644 .changeset/rename-cache-option-to-cache-api.md create mode 100644 .changeset/vitest-pool-drop-sticky-blobs.md create mode 100644 .changeset/wrangler-unstable-worker-options-container-engine.md create mode 100644 packages/miniflare/test/fixtures/echo-plugin/index.ts diff --git a/.changeset/consolidate-miniflare-persist-options.md b/.changeset/consolidate-miniflare-persist-options.md new file mode 100644 index 00000000000..00c2d817366 --- /dev/null +++ b/.changeset/consolidate-miniflare-persist-options.md @@ -0,0 +1,16 @@ +--- +"miniflare": major +--- + +Consolidate persistence and temporary directory options + +The per-resource persistence options (`kvPersist`, `r2Persist`, `d1Persist`, `cachePersist`, `durableObjectsPersist`, `workflowsPersist`, `secretsStorePersist`, `analyticsEngineDatasetsPersist`, `streamPersist`, `imagesPersist`, and `helloWorldPersist`) have been removed, along with `defaultPersistRoot` and `defaultProjectTmpPath`. Persistence is now configured with two options: + +```js +new Miniflare({ + resourcePersistencePath: ".wrangler/state/v3", + resourceTmpPath: ".wrangler/tmp", +}); +``` + +When `resourcePersistencePath` is set, each resource persists to a subdirectory named after its plugin (e.g. `.wrangler/state/v3/kv`). When it is omitted, resources are ephemeral and their data is cleared on dispose. The `boolean`, `memory:`, `file:` and relative-path forms of the old per-resource options, the `.mf` default directory, and the `Miniflare.unsafeGetPersistPaths()` method have also been removed. diff --git a/.changeset/drop-cache-warn-usage.md b/.changeset/drop-cache-warn-usage.md new file mode 100644 index 00000000000..fad1959bc42 --- /dev/null +++ b/.changeset/drop-cache-warn-usage.md @@ -0,0 +1,7 @@ +--- +"miniflare": major +--- + +Remove the `cacheWarnUsage` option + +The `cacheWarnUsage` Worker option, which logged a warning when cache operations were used, has been removed. diff --git a/.changeset/drop-fetch-mock.md b/.changeset/drop-fetch-mock.md new file mode 100644 index 00000000000..899ea332b12 --- /dev/null +++ b/.changeset/drop-fetch-mock.md @@ -0,0 +1,5 @@ +--- +"miniflare": major +--- + +Remove the `fetchMock` option and `createFetchMock` export diff --git a/.changeset/drop-https-key-cert-path.md b/.changeset/drop-https-key-cert-path.md new file mode 100644 index 00000000000..89854346f76 --- /dev/null +++ b/.changeset/drop-https-key-cert-path.md @@ -0,0 +1,7 @@ +--- +"miniflare": major +--- + +Remove the `httpsKeyPath` and `httpsCertPath` options + +The `httpsKeyPath` and `httpsCertPath` options have been removed. To use a custom certificate, read the files and pass their contents via the existing `httpsKey` and `httpsCert` options. diff --git a/.changeset/drop-runtime-stdio-and-structured-logs-option.md b/.changeset/drop-runtime-stdio-and-structured-logs-option.md new file mode 100644 index 00000000000..2aa033314b2 --- /dev/null +++ b/.changeset/drop-runtime-stdio-and-structured-logs-option.md @@ -0,0 +1,9 @@ +--- +"miniflare": major +--- + +Only support structured workerd logs + +The `handleRuntimeStdio` option (for handling the raw `workerd` stdout/stderr streams) and the `structuredWorkerdLogs` option (for toggling structured `workerd` logs) have been removed. Structured logging is now always enabled. + +To receive `workerd`'s output, use `handleStructuredLogs`, which is passed parsed structured log entries. When no `handleStructuredLogs` handler is provided, logs are written to the console by default (`warn`/`error` to stderr, everything else to stdout). diff --git a/.changeset/drop-unsafe-sticky-blobs.md b/.changeset/drop-unsafe-sticky-blobs.md new file mode 100644 index 00000000000..6c6bc1949d1 --- /dev/null +++ b/.changeset/drop-unsafe-sticky-blobs.md @@ -0,0 +1,7 @@ +--- +"miniflare": major +--- + +Drop the `unsafeStickyBlobs` option + +This prevented blob files from being deleted when overwriting or deleting keys, and only existed to support the Durable Object isolated storage feature in `@cloudflare/vitest-pool-workers`, which was removed in 0.13.0. Blobs are now always cleaned up as expected. diff --git a/.changeset/drop-wrapped-bindings.md b/.changeset/drop-wrapped-bindings.md new file mode 100644 index 00000000000..aac9471e5c2 --- /dev/null +++ b/.changeset/drop-wrapped-bindings.md @@ -0,0 +1,5 @@ +--- +"miniflare": major +--- + +Remove the `wrappedBindings` option diff --git a/.changeset/miniflare-container-engine-shared-option.md b/.changeset/miniflare-container-engine-shared-option.md new file mode 100644 index 00000000000..6cd5b07acfc --- /dev/null +++ b/.changeset/miniflare-container-engine-shared-option.md @@ -0,0 +1,16 @@ +--- +"miniflare": major +--- + +Move `containerEngine` from a per-worker option to a top-level option + +`containerEngine` is now a top-level Miniflare option rather than a per-worker option, reflecting that it configures a single container engine for the whole Miniflare instance. + +```js +new Miniflare({ + containerEngine: "unix:///var/run/docker.sock", + workers: [{ name: "my-worker", script: "..." }], +}); +``` + +Previously it was set inside each worker's options. diff --git a/.changeset/rename-cache-option-to-cache-api.md b/.changeset/rename-cache-option-to-cache-api.md new file mode 100644 index 00000000000..7ea88c8cf5d --- /dev/null +++ b/.changeset/rename-cache-option-to-cache-api.md @@ -0,0 +1,7 @@ +--- +"miniflare": major +--- + +Rename the `cache` Worker option to `cacheAPI` + +The boolean option controlling whether the Cache API caches anything has been renamed from `cache` to `cacheAPI`. This is to distinguish it from Workers Cache. diff --git a/.changeset/vitest-pool-drop-sticky-blobs.md b/.changeset/vitest-pool-drop-sticky-blobs.md new file mode 100644 index 00000000000..53a53c3dfb3 --- /dev/null +++ b/.changeset/vitest-pool-drop-sticky-blobs.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/vitest-pool-workers": patch +--- + +Stop enabling Miniflare's removed `unsafeStickyBlobs` option + +The pool no longer sets the `unsafeStickyBlobs` Miniflare option, which has been removed. This option was only needed for the Durable Object isolated storage feature that was dropped in 0.13.0, so there is no change in behaviour. diff --git a/.changeset/wrangler-unstable-worker-options-container-engine.md b/.changeset/wrangler-unstable-worker-options-container-engine.md new file mode 100644 index 00000000000..7f73a13a60b --- /dev/null +++ b/.changeset/wrangler-unstable-worker-options-container-engine.md @@ -0,0 +1,7 @@ +--- +"wrangler": minor +--- + +Remove `containerEngine` from the worker options returned by `unstable_getMiniflareWorkerOptions` + +`unstable_getMiniflareWorkerOptions` no longer includes `containerEngine` in the returned `workerOptions`, since the container engine is a Miniflare instance-wide setting rather than a per-worker one. Callers that build a Miniflare instance from these options should set `containerEngine` at the top level instead. diff --git a/fixtures/unsafe-external-plugin/src/plugins/unsafe-service.ts b/fixtures/unsafe-external-plugin/src/plugins/unsafe-service.ts index 1218e9aaf6b..d901e5e1a0f 100644 --- a/fixtures/unsafe-external-plugin/src/plugins/unsafe-service.ts +++ b/fixtures/unsafe-external-plugin/src/plugins/unsafe-service.ts @@ -57,12 +57,7 @@ export const UNSAFE_SERVICE_PLUGIN: Plugin< options?.map((binding) => [binding.name, new ProxyNodeBinding()]) ?? [] ); }, - async getServices({ - options, - tmpPath, - defaultPersistRoot, - unsafeStickyBlobs, - }) { + async getServices({ options, tmpPath, resourcePersistencePath }) { if (!options || options.length === 0) { return []; } @@ -70,8 +65,7 @@ export const UNSAFE_SERVICE_PLUGIN: Plugin< const persistPath = getPersistPath( UNSAFE_PLUGIN_NAME, tmpPath, - defaultPersistRoot, - undefined + resourcePersistencePath ); await fs.mkdir(persistPath, { recursive: true }); @@ -110,7 +104,7 @@ export const UNSAFE_SERVICE_PLUGIN: Plugin< name: SharedBindings.MAYBE_SERVICE_LOOPBACK, service: { name: SERVICE_LOOPBACK }, }, - ...getMiniflareObjectBindings(unsafeStickyBlobs), + ...getMiniflareObjectBindings(), ], }, } satisfies Service; diff --git a/packages/miniflare/scripts/build.mjs b/packages/miniflare/scripts/build.mjs index bdd44cbe30a..36fa200d169 100644 --- a/packages/miniflare/scripts/build.mjs +++ b/packages/miniflare/scripts/build.mjs @@ -104,6 +104,7 @@ const localExplorerWorkerPath = path.join( */ const fixtureBuilds = [ path.join(pkgRoot, "test/fixtures/unsafe-plugin/index.ts"), + path.join(pkgRoot, "test/fixtures/echo-plugin/index.ts"), ]; /** diff --git a/packages/miniflare/src/http/server.ts b/packages/miniflare/src/http/server.ts index 744d5069dce..b55ceab6f68 100644 --- a/packages/miniflare/src/http/server.ts +++ b/packages/miniflare/src/http/server.ts @@ -1,8 +1,6 @@ -import fs from "node:fs/promises"; import { CERT, KEY } from "./cert"; import type { CORE_PLUGIN } from "../plugins"; import type { HttpOptions, Socket_Https } from "../runtime"; -import type { Awaitable } from "../workers"; import type { z } from "zod"; export async function getEntrySocketHttpOptions( @@ -11,15 +9,9 @@ export async function getEntrySocketHttpOptions( let privateKey: string | undefined = undefined; let certificateChain: string | undefined = undefined; - if ( - (coreOpts.httpsKey || coreOpts.httpsKeyPath) && - (coreOpts.httpsCert || coreOpts.httpsCertPath) - ) { - privateKey = await valueOrFile(coreOpts.httpsKey, coreOpts.httpsKeyPath); - certificateChain = await valueOrFile( - coreOpts.httpsCert, - coreOpts.httpsCertPath - ); + if (coreOpts.httpsKey && coreOpts.httpsCert) { + privateKey = coreOpts.httpsKey; + certificateChain = coreOpts.httpsCert; } else if (coreOpts.https) { privateKey = KEY; certificateChain = CERT; @@ -40,10 +32,3 @@ export async function getEntrySocketHttpOptions( return { http: {} }; } } - -function valueOrFile( - value?: string, - filePath?: string -): Awaitable { - return value ?? (filePath && fs.readFile(filePath, "utf8")); -} diff --git a/packages/miniflare/src/index.ts b/packages/miniflare/src/index.ts index 5d977d7799f..3aac245032e 100644 --- a/packages/miniflare/src/index.ts +++ b/packages/miniflare/src/index.ts @@ -75,7 +75,6 @@ import { getUserServiceName, handlePrettyErrorRequest, JsonErrorSchema, - maybeWrappedModuleToWorkerName, reviveError, } from "./plugins/core"; import { InspectorProxyController } from "./plugins/core/inspector-proxy"; @@ -93,7 +92,6 @@ import { serializeConfig, } from "./runtime"; import { - _isCyclic, isFileNotFoundError, MiniflareCoreError, NoOpLog, @@ -122,7 +120,6 @@ import type { DispatchFetch, RequestInit } from "./http"; import type { DurableObjectClassNames, Plugin, - Plugins, PluginServicesOptions, PluginSharedOptions, PluginWorkerOptions, @@ -131,7 +128,6 @@ import type { ReplaceWorkersTypes, SharedOptions, WorkerOptions, - WrappedBindingNames, } from "./plugins"; import type { NameSourceOptions, @@ -662,47 +658,6 @@ function getExternalServiceEntrypoints(allWorkerOpts: PluginWorkerOptions[]) { return externalServices; } -function invalidWrappedAsBound(name: string, bindingType: string): never { - const stringName = JSON.stringify(name); - throw new MiniflareCoreError( - "ERR_INVALID_WRAPPED", - `Cannot use ${stringName} for wrapped binding because it is bound to with ${bindingType} bindings.\nEnsure other workers don't define ${bindingType} bindings to ${stringName}.` - ); -} -function getWrappedBindingNames( - allWorkerOpts: PluginWorkerOptions[], - durableObjectClassNames: DurableObjectClassNames -): WrappedBindingNames { - // Build set of all worker names bound to as wrapped bindings. - // Also check these "workers" aren't bound to as services/Durable Objects. - // We won't add them as regular workers so these bindings would fail. - const wrappedBindingWorkerNames = new Set(); - for (const workerOpts of allWorkerOpts) { - for (const designator of Object.values( - workerOpts.core.wrappedBindings ?? {} - )) { - const scriptName = - typeof designator === "object" ? designator.scriptName : designator; - if (durableObjectClassNames.has(getUserServiceName(scriptName))) { - invalidWrappedAsBound(scriptName, "Durable Object"); - } - wrappedBindingWorkerNames.add(scriptName); - } - } - // Need to collect all wrapped bindings before checking service bindings - for (const workerOpts of allWorkerOpts) { - for (const designator of Object.values( - workerOpts.core.serviceBindings ?? {} - )) { - if (typeof designator !== "string") continue; - if (wrappedBindingWorkerNames.has(designator)) { - invalidWrappedAsBound(designator, "service"); - } - } - } - return wrappedBindingWorkerNames; -} - function getQueueProducers( allWorkerOpts: PluginWorkerOptions[] ): QueueProducers { @@ -790,13 +745,11 @@ function getQueueConsumers( // Collects all routes from all worker services function getWorkerRoutes( - allWorkerOpts: PluginWorkerOptions[], - wrappedBindingNames: Set + allWorkerOpts: PluginWorkerOptions[] ): Map { const allRoutes = new Map(); for (const workerOpts of allWorkerOpts) { const name = workerOpts.core.name ?? ""; - if (wrappedBindingNames.has(name)) continue; // Wrapped bindings un-routable assert(!allRoutes.has(name)); // Validated unique names earlier allRoutes.set(name, workerOpts.core.routes ?? []); } @@ -1002,8 +955,6 @@ export class Miniflare { #runtimeDispatcher?: Dispatcher; #proxyClient?: ProxyClient; - #structuredWorkerdLogs: boolean; - #cfObject?: Record = {}; // Path to temporary directory for use as scratch space/"in-memory" Durable @@ -1071,11 +1022,6 @@ export class Miniflare { this.#log = this.#sharedOpts.core.log ?? new NoOpLog(); this.#hyperdriveProxyController.log = this.#log; - this.#structuredWorkerdLogs = - this.#sharedOpts.core.structuredWorkerdLogs ?? - // If there is a `handleStructuredLogs` set then `structuredWorkerdLogs` defaults - // to `true`, otherwise it defaults to `false` - (this.#sharedOpts.core.handleStructuredLogs ? true : false); // If we're in a JavaScript Debug terminal, Miniflare will send the inspector ports directly to VSCode for registration // As such, we don't need our inspector proxy and in fact including it causes issue with multiple clients connected to the @@ -1155,7 +1101,7 @@ export class Miniflare { // project temp path is supplied, these live inside `#tmpPath` and are // already removed above. const emailPaths = getEmailPathsToClean( - this.#sharedOpts.core.defaultProjectTmpPath, + this.#sharedOpts.core.resourceTmpPath, this.#tmpPath ); if (emailPaths) { @@ -1332,13 +1278,11 @@ export class Miniflare { ); assert(namespaceId, "Namespace ID is required"); - const doSharedOpts = this.#sharedOpts.do; const coreSharedOpts = this.#sharedOpts.core; const doPersistPath = getPersistPath( DURABLE_OBJECTS_PLUGIN_NAME, this.#tmpPath, - coreSharedOpts.defaultPersistRoot, - doSharedOpts.durableObjectsPersist + coreSharedOpts.resourcePersistencePath ); const namespacePath = path.join(doPersistPath, namespaceId); @@ -1381,13 +1325,11 @@ export class Miniflare { ); assert(workflowName, "Workflow name is required"); - const workflowsSharedOpts = this.#sharedOpts.workflows; const coreSharedOpts = this.#sharedOpts.core; const workflowsPersistPath = getPersistPath( WORKFLOWS_PLUGIN_NAME, this.#tmpPath, - coreSharedOpts.defaultPersistRoot, - workflowsSharedOpts.workflowsPersist + coreSharedOpts.resourcePersistencePath ); // Engine DOs are stored under: /miniflare-workflows-/.sqlite @@ -1462,13 +1404,11 @@ export class Miniflare { assert(workflowName, "Workflow name is required"); - const workflowsSharedOpts = this.#sharedOpts.workflows; const coreSharedOpts = this.#sharedOpts.core; const workflowsPersistPath = getPersistPath( WORKFLOWS_PLUGIN_NAME, this.#tmpPath, - coreSharedOpts.defaultPersistRoot, - workflowsSharedOpts.workflowsPersist + coreSharedOpts.resourcePersistencePath ); const uniqueKey = `miniflare-workflows-${workflowName}`; @@ -1980,10 +1920,6 @@ export class Miniflare { : null; const durableObjectClassNames = getDurableObjectClassNames(allWorkerOpts); - const wrappedBindingNames = getWrappedBindingNames( - allWorkerOpts, - durableObjectClassNames - ); const queueProducers = getQueueProducers(allWorkerOpts); const queueConsumers = getQueueConsumers(allWorkerOpts); // When the dev registry is enabled, queue brokers bind to the dev-registry @@ -1991,7 +1927,7 @@ export class Miniflare { // (see `ExternalQueueProxy`), so the proxy worker must exist whenever // there are queues. const hasQueues = queueProducers.size > 0 || queueConsumers.size > 0; - const allWorkerRoutes = getWorkerRoutes(allWorkerOpts, wrappedBindingNames); + const allWorkerRoutes = getWorkerRoutes(allWorkerOpts); const workerNames = [...allWorkerRoutes.keys()]; // Use Map to dedupe services by name @@ -2030,12 +1966,6 @@ export class Miniflare { */ const proxyBindings: Worker_Binding[] = []; - const allWorkerBindings = new Map(); - const wrappedBindingsToPopulate: { - workerName: string; - innerBindings: Worker_Binding[]; - }[] = []; - for (const [key, plugin] of this.#mergedPluginEntries) { const pluginExtensions = await plugin.getExtensions?.({ // @ts-expect-error `CoreOptionsSchema` has required options which are @@ -2069,7 +1999,6 @@ export class Miniflare { // Collect all bindings from this worker const workerBindings: Worker_Binding[] = []; - allWorkerBindings.set(workerName, workerBindings); const additionalModules: Worker_Module[] = []; for (const [key, plugin] of this.#mergedPluginEntries) { @@ -2103,24 +2032,6 @@ export class Miniflare { if (isNativeTargetBinding(binding)) { proxyBindings.push(buildProxyBinding(key, workerName, binding)); } - // If this is a wrapped binding to a wrapped binding worker, record - // it, so we can populate its inner bindings with all the wrapped - // binding worker's bindings. - if ( - "wrapped" in binding && - binding.wrapped?.moduleName !== undefined && - binding.wrapped.innerBindings !== undefined - ) { - const workerName = maybeWrappedModuleToWorkerName( - binding.wrapped.moduleName - ); - if (workerName !== undefined) { - wrappedBindingsToPopulate.push({ - workerName, - innerBindings: binding.wrapped.innerBindings, - }); - } - } if ("service" in binding) { const targetWorkerName = binding.service?.name?.replace( "core:user:", @@ -2146,7 +2057,6 @@ export class Miniflare { } // Collect all services required by this worker - const unsafeStickyBlobs = sharedOpts.core.unsafeStickyBlobs ?? false; const unsafeEphemeralDurableObjects = workerOpts.core.unsafeEphemeralDurableObjects ?? false; // Store publicUrl so the /core/public-url loopback route can return it. @@ -2163,14 +2073,12 @@ export class Miniflare { workerIndex: i, additionalModules, tmpPath: this.#tmpPath, - defaultPersistRoot: sharedOpts.core.defaultPersistRoot, - defaultProjectTmpPath: sharedOpts.core.defaultProjectTmpPath, + resourcePersistencePath: sharedOpts.core.resourcePersistencePath, + resourceTmpPath: sharedOpts.core.resourceTmpPath, workerNames, loopbackHost, loopbackPort, publicUrl: sharedOpts.core.publicUrl, - unsafeStickyBlobs, - wrappedBindingNames, durableObjectClassNames, unsafeEphemeralDurableObjects, queueProducers, @@ -2393,35 +2301,16 @@ export class Miniflare { services.set(service.name, service); } - // Populate wrapped binding inner bindings with bound worker's bindings - for (const toPopulate of wrappedBindingsToPopulate) { - const bindings = allWorkerBindings.get(toPopulate.workerName); - if (bindings === undefined) continue; - const existingBindingNames = new Set( - toPopulate.innerBindings.map(({ name }) => name) - ); - toPopulate.innerBindings.push( - // If there's already an inner binding with this name, don't add again - ...bindings.filter(({ name }) => !existingBindingNames.has(name)) - ); - } - // If we populated wrapped bindings, we may have created cycles in the - // `services` array. Attempting to serialise these will lead to unbounded - // recursion, so make sure we don't have any const servicesArray = Array.from(services.values()); - if (wrappedBindingsToPopulate.length > 0 && _isCyclic(servicesArray)) { - throw new MiniflareCoreError( - "ERR_CYCLIC", - "Generated workerd config contains cycles. " + - "Ensure wrapped bindings don't have bindings to themselves." - ); - } return { services: servicesArray, sockets, extensions, - structuredLogging: this.#structuredWorkerdLogs, + // Structured logging is always enabled: workerd emits structured logs + // that Miniflare parses and either forwards to a `handleStructuredLogs` + // handler or writes to the console by default. + structuredLogging: true, autogates: process.env.MINIFLARE_WORKERD_AUTOGATES ? process.env.MINIFLARE_WORKERD_AUTOGATES.split(" ") : [], @@ -2510,7 +2399,6 @@ export class Miniflare { ? "127.0.0.1:0" : undefined, verbose: this.#sharedOpts.core.verbose, - handleRuntimeStdio: this.#sharedOpts.core.handleRuntimeStdio, handleStructuredLogs: this.#sharedOpts.core.handleStructuredLogs, runtimeEnv: this.#sharedOpts.core.unsafeRuntimeEnv, }; @@ -2845,9 +2733,6 @@ export class Miniflare { this.#workerOpts = workerOpts; this.#log = this.#sharedOpts.core.log ?? this.#log; this.#hyperdriveProxyController.log = this.#log; - this.#structuredWorkerdLogs = - this.#sharedOpts.core.structuredWorkerdLogs ?? - this.#structuredWorkerdLogs; const newExternalOnUpdate = sharedOpts.core.unsafeHandleDevRegistryUpdate; await this.#devRegistry.updateRegistryPath( @@ -3029,16 +2914,13 @@ export class Miniflare { const bindingName = CoreBindings.SERVICE_USER_ROUTE_PREFIX + workerName; const fetcher = proxyClient.env[bindingName]; - if (fetcher === undefined) { - // `#findAndAssertWorkerIndex()` will throw if a "worker" doesn't exist - // with the specified name. If this "worker" was used as a wrapped binding - // though, it won't be added as a service binding, and so will be - // undefined here. In this case, throw a more specific error. - const stringName = JSON.stringify(workerName); - throw new TypeError( - `${stringName} is being used as a wrapped binding, and cannot be accessed as a worker` - ); - } + // `#findAndAssertWorkerIndex()` throws if a worker doesn't exist with the + // specified name, so by this point the worker is guaranteed to have a + // corresponding route service binding. + assert( + fetcher !== undefined, + `Expected ${bindingName} service binding for worker ${JSON.stringify(workerName)}` + ); return fetcher as ReplaceWorkersTypes; } @@ -3249,8 +3131,7 @@ export class Miniflare { const durableObjectsPersistPath = getPersistPath( DURABLE_OBJECTS_PLUGIN_NAME, this.#tmpPath, - this.#sharedOpts.core.defaultPersistRoot, - this.#sharedOpts.do.durableObjectsPersist + this.#sharedOpts.core.resourcePersistencePath ); try { @@ -3354,17 +3235,6 @@ export class Miniflare { return this.#getProxy(`${pluginName}-internal`, className, serviceName); } - unsafeGetPersistPaths(): Map { - const result = new Map(); - for (const [key, plugin] of PLUGIN_ENTRIES) { - const sharedOpts = this.#sharedOpts[key]; - // @ts-expect-error `sharedOptions` will match the plugin's type here - const maybePath = plugin.getPersistPath?.(sharedOpts, this.#tmpPath); - if (maybePath !== undefined) result.set(key, maybePath); - } - return result; - } - get #mergedPluginEntries() { return [...PLUGIN_ENTRIES, ...this.#externalPlugins.entries()]; } @@ -3415,7 +3285,7 @@ export class Miniflare { // project temp path is supplied, these live inside `#tmpPath` and are // already removed above. const emailPaths = getEmailPathsToClean( - this.#sharedOpts.core.defaultProjectTmpPath, + this.#sharedOpts.core.resourceTmpPath, this.#tmpPath ); if (emailPaths) { diff --git a/packages/miniflare/src/plugins/analytics-engine/index.ts b/packages/miniflare/src/plugins/analytics-engine/index.ts index 98e48db4824..74fcc66030c 100644 --- a/packages/miniflare/src/plugins/analytics-engine/index.ts +++ b/packages/miniflare/src/plugins/analytics-engine/index.ts @@ -1,6 +1,6 @@ import ANALYTICS_ENGINE from "worker:analytics-engine/analytics-engine"; import { z } from "zod"; -import { PersistenceSchema, ProxyNodeBinding } from "../shared"; +import { ProxyNodeBinding } from "../shared"; import type { Worker_Binding } from "../../runtime"; import type { Plugin } from "../shared"; @@ -15,18 +15,12 @@ export const AnalyticsEngineSchemaOptionsSchema = z.object({ analyticsEngineDatasets: AnalyticsEngineSchema.optional(), }); -export const AnalyticsEngineSchemaSharedOptionsSchema = z.object({ - analyticsEngineDatasetsPersist: PersistenceSchema, -}); - export const ANALYTICS_ENGINE_PLUGIN_NAME = "analytics-engine"; export const ANALYTICS_ENGINE_PLUGIN: Plugin< - typeof AnalyticsEngineSchemaOptionsSchema, - typeof AnalyticsEngineSchemaSharedOptionsSchema + typeof AnalyticsEngineSchemaOptionsSchema > = { options: AnalyticsEngineSchemaOptionsSchema, - sharedOptions: AnalyticsEngineSchemaSharedOptionsSchema, bindingTypeDescription: "Analytics Engine dataset", async getBindings(options) { if (!options.analyticsEngineDatasets) { diff --git a/packages/miniflare/src/plugins/cache/index.ts b/packages/miniflare/src/plugins/cache/index.ts index 7f74670444c..b7e7192c151 100644 --- a/packages/miniflare/src/plugins/cache/index.ts +++ b/packages/miniflare/src/plugins/cache/index.ts @@ -3,11 +3,10 @@ import SCRIPT_CACHE_OBJECT from "worker:cache/cache"; import SCRIPT_CACHE_ENTRY from "worker:cache/cache-entry"; import SCRIPT_CACHE_ENTRY_NOOP from "worker:cache/cache-entry-noop"; import { z } from "zod"; -import { CacheBindings, SharedBindings } from "../../workers"; +import { SharedBindings } from "../../workers"; import { getMiniflareObjectBindings, getPersistPath, - PersistenceSchema, SERVICE_LOOPBACK, } from "../shared"; import type { @@ -18,11 +17,7 @@ import type { import type { Plugin } from "../shared"; export const CacheOptionsSchema = z.object({ - cache: z.boolean().optional(), - cacheWarnUsage: z.boolean().optional(), -}); -export const CacheSharedOptionsSchema = z.object({ - cachePersist: PersistenceSchema, + cacheAPI: z.boolean().optional(), }); export const CACHE_PLUGIN_NAME = "cache"; @@ -39,12 +34,8 @@ export function getCacheServiceName(workerIndex: number) { return `${CACHE_PLUGIN_NAME}:${workerIndex}`; } -export const CACHE_PLUGIN: Plugin< - typeof CacheOptionsSchema, - typeof CacheSharedOptionsSchema -> = { +export const CACHE_PLUGIN: Plugin = { options: CacheOptionsSchema, - sharedOptions: CacheSharedOptionsSchema, getBindings() { return []; }, @@ -52,15 +43,12 @@ export const CACHE_PLUGIN: Plugin< return {}; }, async getServices({ - sharedOptions, options, workerIndex, tmpPath, - defaultPersistRoot, - unsafeStickyBlobs, + resourcePersistencePath, }) { - const cache = options.cache ?? true; - const cacheWarnUsage = options.cacheWarnUsage ?? false; + const cache = options.cacheAPI ?? true; let entryWorker: Worker; if (cache) { @@ -75,10 +63,6 @@ export const CACHE_PLUGIN: Plugin< name: SharedBindings.DURABLE_OBJECT_NAMESPACE_OBJECT, durableObjectNamespace: CACHE_OBJECT, }, - { - name: CacheBindings.MAYBE_JSON_CACHE_WARN_USAGE, - json: JSON.stringify(cacheWarnUsage), - }, ], }; } else { @@ -100,12 +84,10 @@ export const CACHE_PLUGIN: Plugin< if (cache) { const uniqueKey = `miniflare-${CACHE_OBJECT_CLASS_NAME}`; - const persist = sharedOptions.cachePersist; const persistPath = getPersistPath( CACHE_PLUGIN_NAME, tmpPath, - defaultPersistRoot, - persist + resourcePersistencePath ); await fs.mkdir(persistPath, { recursive: true }); const storageService: Service = { @@ -141,7 +123,7 @@ export const CACHE_PLUGIN: Plugin< name: SharedBindings.MAYBE_SERVICE_LOOPBACK, service: { name: SERVICE_LOOPBACK }, }, - ...getMiniflareObjectBindings(unsafeStickyBlobs), + ...getMiniflareObjectBindings(), ], }, }; @@ -153,7 +135,4 @@ export const CACHE_PLUGIN: Plugin< return services; }, - getPersistPath({ cachePersist }, tmpPath) { - return getPersistPath(CACHE_PLUGIN_NAME, tmpPath, undefined, cachePersist); - }, }; diff --git a/packages/miniflare/src/plugins/core/index.ts b/packages/miniflare/src/plugins/core/index.ts index 6cdae27f553..75d4341d814 100644 --- a/packages/miniflare/src/plugins/core/index.ts +++ b/packages/miniflare/src/plugins/core/index.ts @@ -10,17 +10,14 @@ import { } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; -import { Readable } from "node:stream"; import tls from "node:tls"; import { TextEncoder } from "node:util"; import { DEFAULT_CONTAINER_EGRESS_INTERCEPTOR_IMAGE } from "@cloudflare/containers-shared"; import { getTodaysCompatDate, removeDirSync } from "@cloudflare/workers-utils"; -import { MockAgent } from "undici"; import SCRIPT_DEV_CONTROL from "worker:core/dev-control"; import SCRIPT_ENTRY from "worker:core/entry"; import OUTBOUND_WORKER from "worker:core/outbound"; import { z } from "zod"; -import { fetch } from "../../http"; import { kVoid } from "../../runtime"; import { JsonSchema, Log, MiniflareCoreError, PathSchema } from "../../shared"; import { getDevControlDurableObjectBindingName } from "../../shared/dev-control"; @@ -123,16 +120,6 @@ if (process.env.NODE_EXTRA_CA_CERTS !== undefined) { const encoder = new TextEncoder(); -export function createFetchMock() { - return new MockAgent(); -} - -const WrappedBindingSchema = z.object({ - scriptName: z.string(), - entrypoint: z.string().optional(), - bindings: z.record(z.string(), JsonSchema).optional(), -}); - // Validate as string, but don't include in parsed output const UnusableStringSchema = z.string().transform(() => undefined); @@ -171,12 +158,8 @@ const CoreOptionsSchemaInput = z.intersection( .record(z.string(), z.union([PathSchema, z.instanceof(Uint8Array)])) .optional(), serviceBindings: z.record(z.string(), ServiceDesignatorSchema).optional(), - wrappedBindings: z - .record(z.string(), z.union([z.string(), WrappedBindingSchema])) - .optional(), outboundService: ServiceDesignatorSchema.optional(), - fetchMock: z.instanceof(MockAgent).optional(), // TODO(soon): remove this in favour of per-object `unsafeUniqueKey: kEphemeralUniqueKey` unsafeEphemeralDurableObjects: z.boolean().optional(), @@ -215,19 +198,6 @@ const CoreOptionsSchemaInput = z.intersection( // If not specified, defaults to `${worker-name}.example.com` zone: z.string().optional(), - /** Configuration used to connect to the container engine */ - containerEngine: z - .union([ - z.object({ - localDocker: z.object({ - socketPath: z.string(), - containerEgressInterceptorImage: z.string().optional(), - }), - }), - z.string(), - ]) - .optional(), - unsafeBindings: z .array( z.object({ @@ -240,24 +210,7 @@ const CoreOptionsSchemaInput = z.intersection( .optional(), }) ); -export const CoreOptionsSchema = CoreOptionsSchemaInput.transform((value) => { - const fetchMock = value.fetchMock; - if (fetchMock !== undefined) { - if (value.outboundService !== undefined) { - throw new MiniflareCoreError( - "ERR_MULTIPLE_OUTBOUNDS", - "Only one of `outboundService` or `fetchMock` may be specified per worker" - ); - } - - // The `fetchMock` option is used to construct the `outboundService` only - // Removing it from the output allows us to re-parse the options later - // This allows us to validate the options and then feed them into Miniflare without issue. - value.fetchMock = undefined; - value.outboundService = (req) => fetch(req, { dispatcher: fetchMock }); - } - return value; -}); +export const CoreOptionsSchema = CoreOptionsSchemaInput; export type WorkerdStructuredLog = z.infer; @@ -267,126 +220,113 @@ export const WorkerdStructuredLogSchema = z.object({ message: z.string(), }); -export const CoreSharedOptionsSchema = z - .object({ - rootPath: UnusableStringSchema.optional(), - - host: z.string().optional(), - port: z.number().optional(), - - https: z.boolean().optional(), - httpsKey: z.string().optional(), - httpsKeyPath: z.string().optional(), - httpsCert: z.string().optional(), - httpsCertPath: z.string().optional(), - - inspectorPort: z.number().optional(), - inspectorHost: z.string().optional(), - - verbose: z.boolean().optional(), - - log: z.instanceof(Log).optional(), - handleRuntimeStdio: z - .function({ - input: [z.instanceof(Readable), z.instanceof(Readable)], - }) - .optional(), - - handleStructuredLogs: z - .function({ - input: [WorkerdStructuredLogSchema], - output: z.void(), - }) - .optional(), - - // Deliberately not `z.function()`: parsing that schema replaces the - // callback with a validating wrapper. When the callback is `async` - // (assignable to a `void` return), the wrapper calls it, rejects the - // returned promise as an invalid `void` return value, and drops that - // promise un-awaited — so if the callback later rejects, no caller - // holds the promise and the rejection crashes the process as an - // unhandled rejection. `z.custom()` passes the function through - // unwrapped; the call site in `handlePrettyErrorRequest` contains - // both throwing and rejecting callbacks. - handleUncaughtError: z - .custom<(error: Error) => void>((value) => typeof value === "function") - .optional(), - - upstream: z.string().optional(), - // TODO: add back validation of cf object - cf: z - .union([z.boolean(), z.string(), z.record(z.string(), z.any())]) - .optional(), +export const CoreSharedOptionsSchema = z.object({ + rootPath: UnusableStringSchema.optional(), - // Enable auto service / durable objects discovery with the dev registry - unsafeDevRegistryPath: z.string().optional(), - // Called when external workers this instance depends on are updated in the dev registry - unsafeHandleDevRegistryUpdate: z - .function({ - input: [z.custom()], - }) - .optional(), - // This is a shared secret between a proxy server and miniflare that can be - // passed in a header to prove that the request came from the proxy and not - // some malicious attacker. - unsafeProxySharedSecret: z.string().optional(), - unsafeModuleFallbackService: CustomFetchServiceSchema.optional(), - // Keep blobs when deleting/overwriting keys, required for stacked storage - unsafeStickyBlobs: z.boolean().optional(), - // Enable directly triggering user Worker handlers with paths like `/cdn-cgi/local/scheduled` - unsafeTriggerHandlers: z.boolean().optional(), - // Extra environment variables to set on the spawned `workerd` subprocess. - // Merged on top of `process.env` and Miniflare's own defaults - // (e.g. `TZ=UTC`, `FORCE_COLOR`), so callers can override those defaults - // (for example, to test timezone-dependent behaviour). - unsafeRuntimeEnv: z.record(z.string(), z.string()).optional(), - // Enable the local explorer at /cdn-cgi/local/explorer - unsafeLocalExplorer: z.boolean().optional(), - // Enable RPC-based Durable Object introspection APIs - unsafeInspectDurableObjects: z.boolean().optional(), - // Enable logging requests - logRequests: z.boolean().default(true), - - // Path to the root directory for persisting data - // Used as the default for all plugins with the plugin name as the subdirectory name - defaultPersistRoot: z.string().optional(), - // Path to the project temporary directory for plugins that need it - // (e.g. email logs). Falls back to a subdirectory of tmpPath if not set. - defaultProjectTmpPath: z.string().optional(), - // Strip the MF-DISABLE_PRETTY_ERROR header from user request - stripDisablePrettyError: z.boolean().default(true), - - // Whether to get structured logs from workerd or not (defaults to `true` is a - // `handleStructuredLogs` is set, to `false` otherwise) - // This option is useful in combination with a custom handleRuntimeStdio. - structuredWorkerdLogs: z.boolean().optional(), - - // Enable telemetry for the local explorer. - telemetry: z - .object({ - enabled: z.boolean().default(false), - deviceId: z.string().optional(), - }) - .default({ enabled: false }), + host: z.string().optional(), + port: z.number().optional(), - // The stable, externally-reachable URL for this Miniflare instance - // (e.g. the Wrangler proxy URL or Vite dev server URL). Used by - // plugins like Stream to generate preview URLs that outlive runtime - // restarts. If not set, plugins fall back to the runtime entry URL. - publicUrl: z.url().optional(), - }) - .refine( - ({ structuredWorkerdLogs, handleStructuredLogs }) => { - if (structuredWorkerdLogs === false && handleStructuredLogs) { - return false; - } - return true; - }, - { - message: - "A `handleStructuredLogs` has been provided but `structuredWorkerdLogs` is set to `false`", - } - ); + https: z.boolean().optional(), + httpsKey: z.string().optional(), + httpsCert: z.string().optional(), + + inspectorPort: z.number().optional(), + inspectorHost: z.string().optional(), + + verbose: z.boolean().optional(), + + log: z.instanceof(Log).optional(), + + handleStructuredLogs: z + .function({ + input: [WorkerdStructuredLogSchema], + output: z.void(), + }) + .optional(), + + // Deliberately not `z.function()`: parsing that schema replaces the + // callback with a validating wrapper. When the callback is `async` + // (assignable to a `void` return), the wrapper calls it, rejects the + // returned promise as an invalid `void` return value, and drops that + // promise un-awaited — so if the callback later rejects, no caller + // holds the promise and the rejection crashes the process as an + // unhandled rejection. `z.custom()` passes the function through + // unwrapped; the call site in `handlePrettyErrorRequest` contains + // both throwing and rejecting callbacks. + handleUncaughtError: z + .custom<(error: Error) => void>((value) => typeof value === "function") + .optional(), + + upstream: z.string().optional(), + // TODO: add back validation of cf object + cf: z + .union([z.boolean(), z.string(), z.record(z.string(), z.any())]) + .optional(), + + // Enable auto service / durable objects discovery with the dev registry + unsafeDevRegistryPath: z.string().optional(), + // Called when external workers this instance depends on are updated in the dev registry + unsafeHandleDevRegistryUpdate: z + .function({ + input: [z.custom()], + }) + .optional(), + // This is a shared secret between a proxy server and miniflare that can be + // passed in a header to prove that the request came from the proxy and not + // some malicious attacker. + unsafeProxySharedSecret: z.string().optional(), + unsafeModuleFallbackService: CustomFetchServiceSchema.optional(), + // Enable directly triggering user Worker handlers with paths like `/cdn-cgi/local/scheduled` + unsafeTriggerHandlers: z.boolean().optional(), + // Extra environment variables to set on the spawned `workerd` subprocess. + // Merged on top of `process.env` and Miniflare's own defaults + // (e.g. `TZ=UTC`, `FORCE_COLOR`), so callers can override those defaults + // (for example, to test timezone-dependent behaviour). + unsafeRuntimeEnv: z.record(z.string(), z.string()).optional(), + // Enable the local explorer at /cdn-cgi/local/explorer + unsafeLocalExplorer: z.boolean().optional(), + // Enable RPC-based Durable Object introspection APIs + unsafeInspectDurableObjects: z.boolean().optional(), + // Enable logging requests + logRequests: z.boolean().default(true), + + // Path to the root directory for persisting resource data (e.g. `.wrangler/state/v3`). + // Each plugin persists under a subdirectory named after the plugin. When unset, + // persistence is disabled and data is stored in an ephemeral tmp directory. + resourcePersistencePath: z.string().optional(), + // Path to the project temporary directory for plugins that need it + // (e.g. `.wrangler/tmp` for email logs). Falls back to a subdirectory of tmpPath if not set. + resourceTmpPath: z.string().optional(), + // Strip the MF-DISABLE_PRETTY_ERROR header from user request + stripDisablePrettyError: z.boolean().default(true), + + // Enable telemetry for the local explorer. + telemetry: z + .object({ + enabled: z.boolean().default(false), + deviceId: z.string().optional(), + }) + .default({ enabled: false }), + + // The stable, externally-reachable URL for this Miniflare instance + // (e.g. the Wrangler proxy URL or Vite dev server URL). Used by + // plugins like Stream to generate preview URLs that outlive runtime + // restarts. If not set, plugins fall back to the runtime entry URL. + publicUrl: z.url().optional(), + + /** Configuration used to connect to the container engine */ + containerEngine: z + .union([ + z.object({ + localDocker: z.object({ + socketPath: z.string(), + containerEgressInterceptorImage: z.string().optional(), + }), + }), + z.string(), + ]) + .optional(), +}); export const CORE_PLUGIN_NAME = "core"; @@ -583,18 +523,6 @@ function getDevControlBindings( return Array.from(bindings.values()); } -const WRAPPED_MODULE_PREFIX = "miniflare-internal:wrapped:"; -function workerNameToWrappedModule(workerName: string): string { - return WRAPPED_MODULE_PREFIX + workerName; -} -export function maybeWrappedModuleToWorkerName( - name: string -): string | undefined { - if (name.startsWith(WRAPPED_MODULE_PREFIX)) { - return name.substring(WRAPPED_MODULE_PREFIX.length); - } -} - function getOutboundInterceptorName(workerIndex: number) { return `outbound:${workerIndex}`; } @@ -669,29 +597,6 @@ export const CORE_PLUGIN: Plugin< }) ); } - if (options.wrappedBindings !== undefined) { - bindings.push( - ...Object.entries(options.wrappedBindings).map(([name, designator]) => { - // Normalise designator - const isObject = typeof designator === "object"; - const scriptName = isObject ? designator.scriptName : designator; - const entrypoint = isObject ? designator.entrypoint : undefined; - const bindings = isObject ? designator.bindings : undefined; - - // Build binding - const moduleName = workerNameToWrappedModule(scriptName); - const innerBindings = - bindings === undefined ? [] : buildBindings(bindings); - // `scriptName`'s bindings will be added to `innerBindings` when - // assembling the config - return { - name, - wrapped: { moduleName, entrypoint, innerBindings }, - }; - }) - ); - } - if (options.unsafeEvalBinding !== undefined) { bindings.push({ name: options.unsafeEvalBinding, @@ -747,15 +652,6 @@ export const CORE_PLUGIN: Plugin< ]) ); } - if (options.wrappedBindings !== undefined) { - bindingEntries.push( - ...Object.keys(options.wrappedBindings).map((name) => [ - name, - new ProxyNodeBinding(), - ]) - ); - } - return Object.fromEntries(await Promise.all(bindingEntries)); }, async getServices({ @@ -764,7 +660,6 @@ export const CORE_PLUGIN: Plugin< sharedOptions, workerBindings, workerIndex, - wrappedBindingNames, durableObjectClassNames, additionalModules, loopbackHost, @@ -837,119 +732,75 @@ export const CORE_PLUGIN: Plugin< options.compatibilityDate ?? FALLBACK_COMPATIBILITY_DATE ); - const isWrappedBinding = wrappedBindingNames.has(name); - const services: Service[] = []; const extensions: Extension[] = []; - if (isWrappedBinding) { - const stringName = JSON.stringify(name); - function invalidWrapped(reason: string): never { - const message = `Cannot use ${stringName} for wrapped binding because ${reason}`; - throw new MiniflareCoreError("ERR_INVALID_WRAPPED", message); - } - if (workerIndex === 0) { - invalidWrapped( - `it's the entrypoint.\nEnsure ${stringName} isn't the first entry in the \`workers\` array.` - ); - } - if (!("modules" in workerScript)) { - invalidWrapped( - `it's a service worker.\nEnsure ${stringName} sets \`modules\` to \`true\` or an array of modules` - ); - } - if (workerScript.modules.length !== 1) { - invalidWrapped( - `it isn't a single module.\nEnsure ${stringName} doesn't include unbundled \`import\`s.` - ); - } - const firstModule = workerScript.modules[0]; - if (!("esModule" in firstModule)) { - invalidWrapped("it isn't a single ES module"); - } - if (options.compatibilityDate !== undefined) { - invalidWrapped( - "it defines a compatibility date.\nWrapped bindings use the compatibility date of the worker with the binding." - ); - } - if (options.compatibilityFlags?.length) { - invalidWrapped( - "it defines compatibility flags.\nWrapped bindings use the compatibility flags of the worker with the binding." - ); - } - if (options.outboundService !== undefined) { - invalidWrapped( - "it defines an outbound service.\nWrapped bindings use the outbound service of the worker with the binding." - ); - } - // We validate this "worker" isn't bound to for services/Durable Objects - // in `getWrappedBindingNames()`. - - extensions.push({ - modules: [ - { - name: workerNameToWrappedModule(name), - esModule: firstModule.esModule, - internal: true, - }, - ], - }); - } else { - services.push({ - name: serviceName, - worker: { - ...workerScript, - compatibilityDate, - compatibilityFlags: options.compatibilityFlags, - bindings: workerBindings, - durableObjectNamespaces: - classNamesEntries.map( - ([ + services.push({ + name: serviceName, + worker: { + ...workerScript, + compatibilityDate, + compatibilityFlags: options.compatibilityFlags, + bindings: workerBindings, + durableObjectNamespaces: + classNamesEntries.map( + ([ + className, + { + enableSql, + unsafeUniqueKey, + unsafePreventEviction: preventEviction, + container, + }, + ]) => { + const uniqueKey = getDurableObjectUniqueKey( className, - { - enableSql, - unsafeUniqueKey, - unsafePreventEviction: preventEviction, - container, - }, - ]) => { - const uniqueKey = getDurableObjectUniqueKey( - className, - options.name, - unsafeUniqueKey - ); - - return uniqueKey === undefined - ? { - className, - enableSql, - ephemeralLocal: kVoid, - preventEviction, - container, - } - : { - className, - enableSql, - uniqueKey, - preventEviction, - container, - }; - } - ), - durableObjectStorage: - classNamesEntries.length === 0 - ? undefined - : options.unsafeEphemeralDurableObjects - ? { inMemory: kVoid } - : { localDisk: DURABLE_OBJECTS_STORAGE_SERVICE_NAME }, - globalOutbound: { name: getOutboundInterceptorName(workerIndex) }, - cacheApiOutbound: { name: getCacheServiceName(workerIndex) }, - moduleFallback: - options.unsafeUseModuleFallbackService && - sharedOptions.unsafeModuleFallbackService !== undefined - ? `${loopbackHost}:${loopbackPort}` - : undefined, - tails: options.tails?.map((service) => { + options.name, + unsafeUniqueKey + ); + + return uniqueKey === undefined + ? { + className, + enableSql, + ephemeralLocal: kVoid, + preventEviction, + container, + } + : { + className, + enableSql, + uniqueKey, + preventEviction, + container, + }; + } + ), + durableObjectStorage: + classNamesEntries.length === 0 + ? undefined + : options.unsafeEphemeralDurableObjects + ? { inMemory: kVoid } + : { localDisk: DURABLE_OBJECTS_STORAGE_SERVICE_NAME }, + globalOutbound: { name: getOutboundInterceptorName(workerIndex) }, + cacheApiOutbound: { name: getCacheServiceName(workerIndex) }, + moduleFallback: + options.unsafeUseModuleFallbackService && + sharedOptions.unsafeModuleFallbackService !== undefined + ? `${loopbackHost}:${loopbackPort}` + : undefined, + tails: options.tails?.map((service) => { + return getCustomServiceDesignator( + /* referrer */ options.name, + workerIndex, + CustomServiceKind.UNKNOWN, + name, + service, + options.hasAssetsAndIsVitest + ); + }), + streamingTails: options.streamingTails?.map( + (service) => { return getCustomServiceDesignator( /* referrer */ options.name, workerIndex, @@ -958,23 +809,11 @@ export const CORE_PLUGIN: Plugin< service, options.hasAssetsAndIsVitest ); - }), - streamingTails: options.streamingTails?.map( - (service) => { - return getCustomServiceDesignator( - /* referrer */ options.name, - workerIndex, - CustomServiceKind.UNKNOWN, - name, - service, - options.hasAssetsAndIsVitest - ); - } - ), - containerEngine: getContainerEngine(options.containerEngine), - }, - }); - } + } + ), + containerEngine: getContainerEngine(sharedOptions.containerEngine), + }, + }); // Define custom `fetch` services if set if (options.serviceBindings !== undefined) { diff --git a/packages/miniflare/src/plugins/d1/index.ts b/packages/miniflare/src/plugins/d1/index.ts index 1ed036e7fa9..b4bb8c53f75 100644 --- a/packages/miniflare/src/plugins/d1/index.ts +++ b/packages/miniflare/src/plugins/d1/index.ts @@ -11,7 +11,6 @@ import { namespaceEntries, namespaceKeys, objectEntryWorker, - PersistenceSchema, ProxyNodeBinding, remoteProxyClientWorker, SERVICE_LOOPBACK, @@ -42,10 +41,6 @@ export const D1OptionsSchema = z.object({ ]) .optional(), }); -export const D1SharedOptionsSchema = z.object({ - d1Persist: PersistenceSchema, -}); - export const D1_PLUGIN_NAME = "d1"; const D1_STORAGE_SERVICE_NAME = `${D1_PLUGIN_NAME}:storage`; const D1_DATABASE_SERVICE_PREFIX = `${D1_PLUGIN_NAME}:db`; @@ -57,12 +52,8 @@ const D1_DATABASE_OBJECT: Worker_Binding_DurableObjectNamespaceDesignator = { className: D1_DATABASE_OBJECT_CLASS_NAME, }; -export const D1_PLUGIN: Plugin< - typeof D1OptionsSchema, - typeof D1SharedOptionsSchema -> = { +export const D1_PLUGIN: Plugin = { options: D1OptionsSchema, - sharedOptions: D1SharedOptionsSchema, bindingTypeDescription: "D1 database", getBindings(options) { const databases = namespaceEntries(options.d1Databases); @@ -112,14 +103,7 @@ export const D1_PLUGIN: Plugin< databases.map((name) => [name, new ProxyNodeBinding()]) ); }, - async getServices({ - options, - sharedOptions, - tmpPath, - defaultPersistRoot, - unsafeStickyBlobs, - }) { - const persist = sharedOptions.d1Persist; + async getServices({ options, tmpPath, resourcePersistencePath }) { const databases = namespaceEntries(options.d1Databases); const services: Service[] = []; @@ -147,8 +131,7 @@ export const D1_PLUGIN: Plugin< const persistPath = getPersistPath( D1_PLUGIN_NAME, tmpPath, - defaultPersistRoot, - persist + resourcePersistencePath ); await fs.mkdir(persistPath, { recursive: true }); @@ -185,7 +168,7 @@ export const D1_PLUGIN: Plugin< name: SharedBindings.MAYBE_SERVICE_LOOPBACK, service: { name: SERVICE_LOOPBACK }, }, - ...getMiniflareObjectBindings(unsafeStickyBlobs), + ...getMiniflareObjectBindings(), ], }, }; @@ -194,7 +177,4 @@ export const D1_PLUGIN: Plugin< return services; }, - getPersistPath({ d1Persist }, tmpPath) { - return getPersistPath(D1_PLUGIN_NAME, tmpPath, undefined, d1Persist); - }, }; diff --git a/packages/miniflare/src/plugins/do/index.ts b/packages/miniflare/src/plugins/do/index.ts index 9a847d17648..8efbdeadcb6 100644 --- a/packages/miniflare/src/plugins/do/index.ts +++ b/packages/miniflare/src/plugins/do/index.ts @@ -4,7 +4,6 @@ import { getUserServiceName } from "../core"; import { getPersistPath, kUnsafeEphemeralUniqueKey, - PersistenceSchema, ProxyNodeBinding, } from "../shared"; import type { Worker_Binding } from "../../runtime"; @@ -52,9 +51,6 @@ export const DurableObjectsOptionsSchema = z.object({ // These might just be configured via migrations, but should still be allocated storage for e.g. ctx.exports support additionalUnboundDurableObjects: z.array(DurableObject).optional(), }); -export const DurableObjectsSharedOptionsSchema = z.object({ - durableObjectsPersist: PersistenceSchema, -}); export function normaliseDurableObject( designator: NonNullable< @@ -115,11 +111,9 @@ export const DURABLE_OBJECTS_PLUGIN_NAME = "do"; export const DURABLE_OBJECTS_STORAGE_SERVICE_NAME = `${DURABLE_OBJECTS_PLUGIN_NAME}:storage`; export const DURABLE_OBJECTS_PLUGIN: Plugin< - typeof DurableObjectsOptionsSchema, - typeof DurableObjectsSharedOptionsSchema + typeof DurableObjectsOptionsSchema > = { options: DurableObjectsOptionsSchema, - sharedOptions: DurableObjectsSharedOptionsSchema, bindingTypeDescription: "Durable Object namespace", getBindings(options) { return Object.entries(options.durableObjects ?? {}).map( @@ -139,9 +133,8 @@ export const DURABLE_OBJECTS_PLUGIN: Plugin< ); }, async getServices({ - sharedOptions, tmpPath, - defaultPersistRoot, + resourcePersistencePath, durableObjectClassNames, unsafeEphemeralDurableObjects, }) { @@ -164,8 +157,7 @@ export const DURABLE_OBJECTS_PLUGIN: Plugin< const storagePath = getPersistPath( DURABLE_OBJECTS_PLUGIN_NAME, tmpPath, - defaultPersistRoot, - sharedOptions.durableObjectsPersist + resourcePersistencePath ); // `workerd` requires the `disk.path` to exist. Setting `recursive: true` // is like `mkdir -p`: it won't fail if the directory already exists, and it @@ -181,12 +173,4 @@ export const DURABLE_OBJECTS_PLUGIN: Plugin< }, ]; }, - getPersistPath({ durableObjectsPersist }, tmpPath) { - return getPersistPath( - DURABLE_OBJECTS_PLUGIN_NAME, - tmpPath, - undefined, - durableObjectsPersist - ); - }, }; diff --git a/packages/miniflare/src/plugins/email/index.ts b/packages/miniflare/src/plugins/email/index.ts index d46d5c8024f..dfaedfaf3c3 100644 --- a/packages/miniflare/src/plugins/email/index.ts +++ b/packages/miniflare/src/plugins/email/index.ts @@ -57,27 +57,27 @@ function buildJsonBindings(bindings: Record): Worker_Binding[] { } function getEmailProjectParentDirectory( - defaultProjectTmpPath: string | undefined + resourceTmpPath: string | undefined ): string | undefined { - if (defaultProjectTmpPath === undefined) { + if (resourceTmpPath === undefined) { return undefined; } - return path.join(defaultProjectTmpPath, EMAIL_PLUGIN_NAME); + return path.join(resourceTmpPath, EMAIL_PLUGIN_NAME); } /** * Returns the session directory for email files. - * Path: `/email/` + * Path: `/email/` * Example: `/path/to/project/.wrangler/tmp/email/dev-abc123` * When an email is logged, it is stored under this directory using a type indicator * and a unique ID. * Path: `//.` */ function getEmailProjectSessionDirectory( - defaultProjectTmpPath: string | undefined, + resourceTmpPath: string | undefined, tmpPath: string ): string | undefined { - const parentDir = getEmailProjectParentDirectory(defaultProjectTmpPath); + const parentDir = getEmailProjectParentDirectory(resourceTmpPath); if (parentDir === undefined) { return undefined; } @@ -85,17 +85,14 @@ function getEmailProjectSessionDirectory( } export function getEmailPathsToClean( - defaultProjectTmpPath: string | undefined, + resourceTmpPath: string | undefined, tmpPath: string ): { sessionDir: string; parentDir: string } | undefined { - if (defaultProjectTmpPath === undefined) { + if (resourceTmpPath === undefined) { return undefined; } - const sessionDir = getEmailProjectSessionDirectory( - defaultProjectTmpPath, - tmpPath - ); - const parentDir = getEmailProjectParentDirectory(defaultProjectTmpPath); + const sessionDir = getEmailProjectSessionDirectory(resourceTmpPath, tmpPath); + const parentDir = getEmailProjectParentDirectory(resourceTmpPath); if (sessionDir === undefined || parentDir === undefined) { return undefined; } @@ -148,7 +145,7 @@ export const EMAIL_PLUGIN: Plugin = { await mkdir(emailSystemDirectory, { recursive: true }); // Map binding disk services to names and paths, for concise access when storing emails as files. - // When defaultProjectTmpPath is unset, only create system service to avoid duplicates + // When resourceTmpPath is unset, only create system service to avoid duplicates const diskServices: Array<{ location: "system" | "project"; bindingName: string; @@ -163,9 +160,9 @@ export const EMAIL_PLUGIN: Plugin = { }, ]; - if (args.defaultProjectTmpPath) { + if (args.resourceTmpPath) { const emailProjectSessionDirectory = getEmailProjectSessionDirectory( - args.defaultProjectTmpPath, + args.resourceTmpPath, args.tmpPath ); if (emailProjectSessionDirectory !== undefined) { diff --git a/packages/miniflare/src/plugins/hello-world/index.ts b/packages/miniflare/src/plugins/hello-world/index.ts index 3b3d1fc1103..25317de453a 100644 --- a/packages/miniflare/src/plugins/hello-world/index.ts +++ b/packages/miniflare/src/plugins/hello-world/index.ts @@ -6,7 +6,6 @@ import { SharedBindings } from "../../workers"; import { getMiniflareObjectBindings, getPersistPath, - PersistenceSchema, ProxyNodeBinding, SERVICE_LOOPBACK, } from "../shared"; @@ -26,16 +25,8 @@ export const HelloWorldOptionsSchema = z.object({ .optional(), }); -export const HelloWorldSharedOptionsSchema = z.object({ - helloWorldPersist: PersistenceSchema, -}); - -export const HELLO_WORLD_PLUGIN: Plugin< - typeof HelloWorldOptionsSchema, - typeof HelloWorldSharedOptionsSchema -> = { +export const HELLO_WORLD_PLUGIN: Plugin = { options: HelloWorldOptionsSchema, - sharedOptions: HelloWorldSharedOptionsSchema, bindingTypeDescription: "Hello World", async getBindings(options) { if (!options.helloWorld) { @@ -66,13 +57,7 @@ export const HELLO_WORLD_PLUGIN: Plugin< ]) ); }, - async getServices({ - options, - sharedOptions, - tmpPath, - defaultPersistRoot, - unsafeStickyBlobs, - }) { + async getServices({ options, tmpPath, resourcePersistencePath }) { const configs = options.helloWorld ? Object.values(options.helloWorld) : []; if (configs.length === 0) { @@ -82,8 +67,7 @@ export const HELLO_WORLD_PLUGIN: Plugin< const persistPath = getPersistPath( HELLO_WORLD_PLUGIN_NAME, tmpPath, - defaultPersistRoot, - sharedOptions.helloWorldPersist + resourcePersistencePath ); await fs.mkdir(persistPath, { recursive: true }); @@ -120,7 +104,7 @@ export const HELLO_WORLD_PLUGIN: Plugin< name: SharedBindings.MAYBE_SERVICE_LOOPBACK, service: { name: SERVICE_LOOPBACK }, }, - ...getMiniflareObjectBindings(unsafeStickyBlobs), + ...getMiniflareObjectBindings(), ], }, } satisfies Service; @@ -152,12 +136,4 @@ export const HELLO_WORLD_PLUGIN: Plugin< return [...services, storageService, objectService]; }, - getPersistPath(sharedOptions, tmpPath) { - return getPersistPath( - HELLO_WORLD_PLUGIN_NAME, - tmpPath, - undefined, - sharedOptions.helloWorldPersist - ); - }, }; diff --git a/packages/miniflare/src/plugins/images/index.ts b/packages/miniflare/src/plugins/images/index.ts index 7df446df282..aabff361b5d 100644 --- a/packages/miniflare/src/plugins/images/index.ts +++ b/packages/miniflare/src/plugins/images/index.ts @@ -10,7 +10,6 @@ import { getPersistPath, getUserBindingServiceName, objectEntryWorker, - PersistenceSchema, ProxyNodeBinding, remoteProxyClientWorker, SERVICE_LOOPBACK, @@ -30,19 +29,11 @@ export const ImagesOptionsSchema = z.object({ images: ImagesSchema.optional(), }); -export const ImagesSharedOptionsSchema = z.object({ - imagesPersist: PersistenceSchema, -}); - export const IMAGES_PLUGIN_NAME = "images"; const IMAGES_REMOTE_SERVICE_NAME = `${IMAGES_PLUGIN_NAME}:remote`; -export const IMAGES_PLUGIN: Plugin< - typeof ImagesOptionsSchema, - typeof ImagesSharedOptionsSchema -> = { +export const IMAGES_PLUGIN: Plugin = { options: ImagesOptionsSchema, - sharedOptions: ImagesSharedOptionsSchema, bindingTypeDescription: "Images", async getBindings(options) { if (!options.images) { @@ -85,13 +76,7 @@ export const IMAGES_PLUGIN: Plugin< [options.images.binding]: new ProxyNodeBinding(), }; }, - async getServices({ - options, - sharedOptions, - tmpPath, - defaultPersistRoot, - unsafeStickyBlobs, - }) { + async getServices({ options, tmpPath, resourcePersistencePath }) { if (!options.images) { return []; } @@ -113,8 +98,7 @@ export const IMAGES_PLUGIN: Plugin< const persistPath = getPersistPath( IMAGES_PLUGIN_NAME, tmpPath, - defaultPersistRoot, - sharedOptions.imagesPersist + resourcePersistencePath ); await fs.mkdir(persistPath, { recursive: true }); @@ -151,7 +135,7 @@ export const IMAGES_PLUGIN: Plugin< name: SharedBindings.MAYBE_SERVICE_LOOPBACK, service: { name: SERVICE_LOOPBACK }, }, - ...getMiniflareObjectBindings(unsafeStickyBlobs), + ...getMiniflareObjectBindings(), ], }, } satisfies Service; @@ -189,12 +173,4 @@ export const IMAGES_PLUGIN: Plugin< return [storageService, objectService, kvNamespaceService, imagesService]; }, - getPersistPath({ imagesPersist }, tmpPath) { - return getPersistPath( - IMAGES_PLUGIN_NAME, - tmpPath, - undefined, - imagesPersist - ); - }, }; diff --git a/packages/miniflare/src/plugins/index.ts b/packages/miniflare/src/plugins/index.ts index f60738d9d2a..3dca3daa67b 100644 --- a/packages/miniflare/src/plugins/index.ts +++ b/packages/miniflare/src/plugins/index.ts @@ -194,7 +194,6 @@ export { CoreOptionsSchema, CoreSharedOptionsSchema, compileModuleRules, - createFetchMock, getGlobalServices, ModuleRuleTypeSchema, ModuleRuleSchema, diff --git a/packages/miniflare/src/plugins/kv/index.ts b/packages/miniflare/src/plugins/kv/index.ts index 014438790f9..6da8d3a1da6 100644 --- a/packages/miniflare/src/plugins/kv/index.ts +++ b/packages/miniflare/src/plugins/kv/index.ts @@ -10,7 +10,6 @@ import { namespaceEntries, namespaceKeys, objectEntryWorker, - PersistenceSchema, ProxyNodeBinding, remoteProxyClientWorker, SERVICE_LOOPBACK, @@ -53,10 +52,6 @@ export const KVOptionsSchema = z.object({ siteInclude: z.string().array().optional(), siteExclude: z.string().array().optional(), }); -export const KVSharedOptionsSchema = z.object({ - kvPersist: PersistenceSchema, -}); - const SERVICE_NAMESPACE_PREFIX = `${KV_PLUGIN_NAME}:ns`; // A single entry service shared by every *local* namespace. Each namespace's id // is supplied per-binding via `ctx.props`, so one service serves all of them. @@ -76,12 +71,8 @@ function isWorkersSitesEnabled( return options.sitePath !== undefined; } -export const KV_PLUGIN: Plugin< - typeof KVOptionsSchema, - typeof KVSharedOptionsSchema -> = { +export const KV_PLUGIN: Plugin = { options: KVOptionsSchema, - sharedOptions: KVSharedOptionsSchema, bindingTypeDescription: "KV namespace", async getBindings(options) { const namespaces = namespaceEntries(options.kvNamespaces); @@ -135,14 +126,7 @@ export const KV_PLUGIN: Plugin< return bindings; }, - async getServices({ - options, - sharedOptions, - tmpPath, - defaultPersistRoot, - unsafeStickyBlobs, - }) { - const persist = sharedOptions.kvPersist; + async getServices({ options, tmpPath, resourcePersistencePath }) { const namespaces = namespaceEntries(options.kvNamespaces); const services: Service[] = []; @@ -174,8 +158,7 @@ export const KV_PLUGIN: Plugin< const persistPath = getPersistPath( KV_PLUGIN_NAME, tmpPath, - defaultPersistRoot, - persist + resourcePersistencePath ); await fs.mkdir(persistPath, { recursive: true }); const storageService: Service = { @@ -208,7 +191,7 @@ export const KV_PLUGIN: Plugin< name: SharedBindings.MAYBE_SERVICE_LOOPBACK, service: { name: SERVICE_LOOPBACK }, }, - ...getMiniflareObjectBindings(unsafeStickyBlobs), + ...getMiniflareObjectBindings(), ], }, }; @@ -221,10 +204,6 @@ export const KV_PLUGIN: Plugin< return services; }, - - getPersistPath({ kvPersist }, tmpPath) { - return getPersistPath(KV_PLUGIN_NAME, tmpPath, undefined, kvPersist); - }, }; export { KV_PLUGIN_NAME }; diff --git a/packages/miniflare/src/plugins/queues/index.ts b/packages/miniflare/src/plugins/queues/index.ts index ff0afb99d33..959f46d3452 100644 --- a/packages/miniflare/src/plugins/queues/index.ts +++ b/packages/miniflare/src/plugins/queues/index.ts @@ -77,7 +77,6 @@ export const QUEUES_PLUGIN: Plugin = { queueProducers: allQueueProducers, queueConsumers: allQueueConsumers, devRegistryEnabled, - unsafeStickyBlobs, }) { const produced = bindingEntries(options.queueProducers).map(([, id]) => id); // Consumed queues get a broker service even without a local producer so @@ -119,7 +118,7 @@ export const QUEUES_PLUGIN: Plugin = { name: SharedBindings.MAYBE_SERVICE_LOOPBACK, service: { name: SERVICE_LOOPBACK }, }, - ...getMiniflareObjectBindings(unsafeStickyBlobs), + ...getMiniflareObjectBindings(), { name: SharedBindings.DURABLE_OBJECT_NAMESPACE_OBJECT, durableObjectNamespace: { diff --git a/packages/miniflare/src/plugins/r2/index.ts b/packages/miniflare/src/plugins/r2/index.ts index 85a6f63aa16..20e0b69190a 100644 --- a/packages/miniflare/src/plugins/r2/index.ts +++ b/packages/miniflare/src/plugins/r2/index.ts @@ -11,7 +11,6 @@ import { namespaceEntries, namespaceKeys, objectEntryWorker, - PersistenceSchema, ProxyNodeBinding, remoteProxyClientWorker, SERVICE_LOOPBACK, @@ -42,10 +41,6 @@ export const R2OptionsSchema = z.object({ ]) .optional(), }); -export const R2SharedOptionsSchema = z.object({ - r2Persist: PersistenceSchema, -}); - export const R2_PLUGIN_NAME = "r2"; const R2_STORAGE_SERVICE_NAME = `${R2_PLUGIN_NAME}:storage`; const R2_BUCKET_SERVICE_PREFIX = `${R2_PLUGIN_NAME}:bucket`; @@ -89,12 +84,8 @@ export function getR2PublicService( }; } -export const R2_PLUGIN: Plugin< - typeof R2OptionsSchema, - typeof R2SharedOptionsSchema -> = { +export const R2_PLUGIN: Plugin = { options: R2OptionsSchema, - sharedOptions: R2SharedOptionsSchema, bindingTypeDescription: "R2 bucket", getBindings(options) { const buckets = namespaceEntries(options.r2Buckets); @@ -122,14 +113,7 @@ export const R2_PLUGIN: Plugin< buckets.map((name) => [name, new ProxyNodeBinding()]) ); }, - async getServices({ - options, - sharedOptions, - tmpPath, - defaultPersistRoot, - unsafeStickyBlobs, - }) { - const persist = sharedOptions.r2Persist; + async getServices({ options, tmpPath, resourcePersistencePath }) { const buckets = namespaceEntries(options.r2Buckets); const services: Service[] = []; @@ -157,8 +141,7 @@ export const R2_PLUGIN: Plugin< const persistPath = getPersistPath( R2_PLUGIN_NAME, tmpPath, - defaultPersistRoot, - persist + resourcePersistencePath ); await fs.mkdir(persistPath, { recursive: true }); const storageService: Service = { @@ -194,7 +177,7 @@ export const R2_PLUGIN: Plugin< name: SharedBindings.MAYBE_SERVICE_LOOPBACK, service: { name: SERVICE_LOOPBACK }, }, - ...getMiniflareObjectBindings(unsafeStickyBlobs), + ...getMiniflareObjectBindings(), ], }, }; @@ -203,7 +186,4 @@ export const R2_PLUGIN: Plugin< return services; }, - getPersistPath({ r2Persist }, tmpPath) { - return getPersistPath(R2_PLUGIN_NAME, tmpPath, undefined, r2Persist); - }, }; diff --git a/packages/miniflare/src/plugins/ratelimit/index.ts b/packages/miniflare/src/plugins/ratelimit/index.ts index b4dd59b73f2..c42ee653f25 100644 --- a/packages/miniflare/src/plugins/ratelimit/index.ts +++ b/packages/miniflare/src/plugins/ratelimit/index.ts @@ -99,7 +99,7 @@ export const RATELIMIT_PLUGIN: Plugin = { ]) ); }, - async getServices({ options, unsafeStickyBlobs }) { + async getServices({ options }) { if (!options.ratelimits) { return []; } @@ -145,7 +145,7 @@ export const RATELIMIT_PLUGIN: Plugin = { name: SharedBindings.MAYBE_SERVICE_LOOPBACK, service: { name: SERVICE_LOOPBACK }, }, - ...getMiniflareObjectBindings(unsafeStickyBlobs), + ...getMiniflareObjectBindings(), ], }, }); diff --git a/packages/miniflare/src/plugins/secret-store/index.ts b/packages/miniflare/src/plugins/secret-store/index.ts index 27074a797f4..1fbf4b5b900 100644 --- a/packages/miniflare/src/plugins/secret-store/index.ts +++ b/packages/miniflare/src/plugins/secret-store/index.ts @@ -9,7 +9,6 @@ import { getPersistPath, getUserBindingServiceName, objectEntryWorker, - PersistenceSchema, ProxyNodeBinding, SERVICE_LOOPBACK, } from "../shared"; @@ -28,18 +27,12 @@ export const SecretsStoreSecretsOptionsSchema = z.object({ secretsStoreSecrets: SecretsStoreSecretsSchema.optional(), }); -export const SecretsStoreSecretsSharedOptionsSchema = z.object({ - secretsStorePersist: PersistenceSchema, -}); - export const SECRET_STORE_PLUGIN_NAME = "secrets-store"; export const SECRET_STORE_PLUGIN: Plugin< - typeof SecretsStoreSecretsOptionsSchema, - typeof SecretsStoreSecretsSharedOptionsSchema + typeof SecretsStoreSecretsOptionsSchema > = { options: SecretsStoreSecretsOptionsSchema, - sharedOptions: SecretsStoreSecretsSharedOptionsSchema, bindingTypeDescription: "Secrets Store secret", async getBindings(options) { if (!options.secretsStoreSecrets) { @@ -73,13 +66,7 @@ export const SECRET_STORE_PLUGIN: Plugin< ]) ); }, - async getServices({ - options, - sharedOptions, - tmpPath, - defaultPersistRoot, - unsafeStickyBlobs, - }) { + async getServices({ options, tmpPath, resourcePersistencePath }) { const configs = options.secretsStoreSecrets ? Object.values(options.secretsStoreSecrets) : []; @@ -91,8 +78,7 @@ export const SECRET_STORE_PLUGIN: Plugin< const persistPath = getPersistPath( SECRET_STORE_PLUGIN_NAME, tmpPath, - defaultPersistRoot, - sharedOptions.secretsStorePersist + resourcePersistencePath ); await fs.mkdir(persistPath, { recursive: true }); @@ -130,7 +116,7 @@ export const SECRET_STORE_PLUGIN: Plugin< name: SharedBindings.MAYBE_SERVICE_LOOPBACK, service: { name: SERVICE_LOOPBACK }, }, - ...getMiniflareObjectBindings(unsafeStickyBlobs), + ...getMiniflareObjectBindings(), ], }, } satisfies Service; @@ -178,12 +164,4 @@ export const SECRET_STORE_PLUGIN: Plugin< return [...services, storageService, objectService]; }, - getPersistPath({ secretsStorePersist }, tmpPath) { - return getPersistPath( - SECRET_STORE_PLUGIN_NAME, - tmpPath, - undefined, - secretsStorePersist - ); - }, }; diff --git a/packages/miniflare/src/plugins/shared/constants.ts b/packages/miniflare/src/plugins/shared/constants.ts index 02c5c2ef601..6661669d5f8 100644 --- a/packages/miniflare/src/plugins/shared/constants.ts +++ b/packages/miniflare/src/plugins/shared/constants.ts @@ -37,21 +37,12 @@ const WORKER_BINDING_ENABLE_CONTROL_ENDPOINTS: Worker_Binding = { name: SharedBindings.MAYBE_JSON_ENABLE_CONTROL_ENDPOINTS, json: "true", }; -const WORKER_BINDING_ENABLE_STICKY_BLOBS: Worker_Binding = { - name: SharedBindings.MAYBE_JSON_ENABLE_STICKY_BLOBS, - json: "true", -}; let enableControlEndpoints = false; -export function getMiniflareObjectBindings( - unsafeStickyBlobs: boolean -): Worker_Binding[] { +export function getMiniflareObjectBindings(): Worker_Binding[] { const result: Worker_Binding[] = []; if (enableControlEndpoints) { result.push(WORKER_BINDING_ENABLE_CONTROL_ENDPOINTS); } - if (unsafeStickyBlobs) { - result.push(WORKER_BINDING_ENABLE_STICKY_BLOBS); - } return result; } /** @internal */ diff --git a/packages/miniflare/src/plugins/shared/index.ts b/packages/miniflare/src/plugins/shared/index.ts index e65ac9cd1a7..4d61caa0a55 100644 --- a/packages/miniflare/src/plugins/shared/index.ts +++ b/packages/miniflare/src/plugins/shared/index.ts @@ -1,8 +1,7 @@ import { createHash } from "node:crypto"; import path from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; -import { z } from "zod"; -import { MiniflareCoreError, PathSchema } from "../../shared"; +import { pathToFileURL } from "node:url"; +import { MiniflareCoreError } from "../../shared"; import type { Extension, Service, @@ -18,20 +17,7 @@ import type { import type { DOContainerOptions } from "../do"; import type { HyperdriveProxyController } from "../hyperdrive/hyperdrive-proxy"; import type { UnsafeUniqueKey } from "./constants"; - -export const DEFAULT_PERSIST_ROOT = ".mf"; - -export const PersistenceSchema = z - // Zod checks union types in order, both `z.url()` and `PathSchema` - // will result in a `string`, but `PathSchema` gets resolved relative to the - // closest `rootPath`. - .union([z.boolean(), z.url(), PathSchema]) - .optional(); -export type Persistence = z.infer; - -// Set of "worker" names that are being used as wrapped bindings and shouldn't -// be added a regular worker services. These workers shouldn't be routable. -export type WrappedBindingNames = Set; +import type { z } from "zod"; // Maps workflow binding names to their workflow options export interface WorkflowOption { @@ -74,16 +60,14 @@ export interface PluginServicesOptions< workerIndex: number; additionalModules: Worker_Module[]; tmpPath: string; - defaultPersistRoot: string | undefined; - defaultProjectTmpPath: string | undefined; + resourcePersistencePath: string | undefined; + resourceTmpPath: string | undefined; workerNames: string[]; loopbackHost: string; loopbackPort: number; publicUrl: string | undefined; - unsafeStickyBlobs: boolean; // ~~Leaky abstractions~~ "Plugin specific options" :) - wrappedBindingNames: WrappedBindingNames; durableObjectClassNames: DurableObjectClassNames; unsafeEphemeralDurableObjects: boolean; queueProducers: QueueProducers; @@ -117,10 +101,6 @@ export interface PluginBase< getServices( options: PluginServicesOptions ): Awaitable; - getPersistPath?( - sharedOptions: OptionalZodTypeOf, - tmpPath: string - ): string; getExtensions?(options: { options: z.infer[]; }): Awaitable; @@ -220,7 +200,7 @@ export function namespaceEntries( } } -export function maybeParseURL(url: Persistence): URL | undefined { +export function maybeParseURL(url: string | undefined): URL | undefined { if (typeof url !== "string" || path.isAbsolute(url)) return; try { return new URL(url); @@ -230,48 +210,18 @@ export function maybeParseURL(url: Persistence): URL | undefined { export function getPersistPath( pluginName: string, tmpPath: string, - defaultPersistRoot: string | undefined, - persist: Persistence + resourcePersistencePath: string | undefined ): string { - // If persistence is disabled, use "memory" storage. Note we're still - // returning a path on the file-system here. Miniflare 2's in-memory storage - // persisted between options reloads. However, we restart the `workerd` - // process on each reload which would destroy any in-memory data. We'd like to - // keep Miniflare 2's behaviour, so persist to a temporary path which we - // destroy on `dispose()`. - const memoryishPath = path.join(tmpPath, pluginName); - - let result: string; - if (persist === false) { - result = memoryishPath; - } else if (persist === undefined) { - // If `persist` is undefined, use either the default path or fallback to the tmpPath - result = - defaultPersistRoot === undefined - ? memoryishPath - : path.join(defaultPersistRoot, pluginName); - } else { - // Try parse `persist` as a URL - const url = maybeParseURL(persist); - if (url !== undefined) { - if (url.protocol === "memory:") { - result = memoryishPath; - } else if (url.protocol === "file:") { - result = fileURLToPath(url); - } else { - throw new MiniflareCoreError( - "ERR_PERSIST_UNSUPPORTED", - `Unsupported "${url.protocol}" persistence protocol for storage: ${url.href}` - ); - } - } else { - // Otherwise, fallback to file storage - result = - persist === true - ? path.join(defaultPersistRoot ?? DEFAULT_PERSIST_ROOT, pluginName) - : persist; - } - } + // If persistence is disabled (no resource persistence path), use "memory" + // storage. Note we're still returning a path on the file-system here. + // Miniflare 2's in-memory storage persisted between options reloads. However, + // we restart the `workerd` process on each reload which would destroy any + // in-memory data. We'd like to keep Miniflare 2's behaviour, so persist to a + // temporary path which we destroy on `dispose()`. + const result = + resourcePersistencePath === undefined + ? path.join(tmpPath, pluginName) + : path.join(resourcePersistencePath, pluginName); // Normalize to forward slashes for workerd's disk service compatibility on // Windows. workerd is a Unix-oriented C++ program and its disk service does diff --git a/packages/miniflare/src/plugins/stream/index.ts b/packages/miniflare/src/plugins/stream/index.ts index fe75ffbc367..3ef0fc25809 100644 --- a/packages/miniflare/src/plugins/stream/index.ts +++ b/packages/miniflare/src/plugins/stream/index.ts @@ -8,7 +8,6 @@ import { getMiniflareObjectBindings, getPersistPath, getUserBindingServiceName, - PersistenceSchema, ProxyNodeBinding, remoteProxyClientWorker, WORKER_BINDING_SERVICE_LOOPBACK, @@ -27,10 +26,6 @@ export const StreamOptionsSchema = z.object({ stream: StreamSchema.optional(), }); -export const StreamSharedOptionsSchema = z.object({ - streamPersist: PersistenceSchema, -}); - export const STREAM_PLUGIN_NAME = "stream"; const STREAM_REMOTE_SERVICE_NAME = `${STREAM_PLUGIN_NAME}:remote`; const STREAM_STORAGE_SERVICE_NAME = `${STREAM_PLUGIN_NAME}:storage`; @@ -39,12 +34,8 @@ export const STREAM_OBJECT_CLASS_NAME = "StreamObject"; export const STREAM_COMPAT_DATE = "2026-03-23"; -export const STREAM_PLUGIN: Plugin< - typeof StreamOptionsSchema, - typeof StreamSharedOptionsSchema -> = { +export const STREAM_PLUGIN: Plugin = { options: StreamOptionsSchema, - sharedOptions: StreamSharedOptionsSchema, bindingTypeDescription: "Stream", async getBindings(options) { if (!options.stream) { @@ -77,13 +68,7 @@ export const STREAM_PLUGIN: Plugin< [options.stream.binding]: new ProxyNodeBinding(), }; }, - async getServices({ - options, - sharedOptions, - tmpPath, - defaultPersistRoot, - unsafeStickyBlobs, - }) { + async getServices({ options, tmpPath, resourcePersistencePath }) { if (!options.stream) { return []; } @@ -100,8 +85,7 @@ export const STREAM_PLUGIN: Plugin< const persistPath = getPersistPath( STREAM_PLUGIN_NAME, tmpPath, - defaultPersistRoot, - sharedOptions.streamPersist + resourcePersistencePath ); await fs.mkdir(persistPath, { recursive: true }); @@ -136,7 +120,7 @@ export const STREAM_PLUGIN: Plugin< name: SharedBindings.MAYBE_SERVICE_BLOBS, service: { name: STREAM_STORAGE_SERVICE_NAME }, }, - ...getMiniflareObjectBindings(unsafeStickyBlobs), + ...getMiniflareObjectBindings(), ], // Allow the DO to send outbound HTTP requests (fetching watermark images) globalOutbound: { name: "internet" }, @@ -177,12 +161,4 @@ export const STREAM_PLUGIN: Plugin< return [storageService, objectService, bindingService]; }, - getPersistPath({ streamPersist }, tmpPath) { - return getPersistPath( - STREAM_PLUGIN_NAME, - tmpPath, - undefined, - streamPersist - ); - }, }; diff --git a/packages/miniflare/src/plugins/workflows/index.ts b/packages/miniflare/src/plugins/workflows/index.ts index b54ccfdcd6e..8d5d7cc803a 100644 --- a/packages/miniflare/src/plugins/workflows/index.ts +++ b/packages/miniflare/src/plugins/workflows/index.ts @@ -6,7 +6,6 @@ import { getUserServiceName } from "../core"; import { getPersistPath, getUserBindingServiceName, - PersistenceSchema, ProxyNodeBinding, SERVICE_DEV_REGISTRY_PROXY, } from "../shared"; @@ -37,19 +36,11 @@ export const WorkflowsOptionsSchema = z.object({ ) .optional(), }); -export const WorkflowsSharedOptionsSchema = z.object({ - workflowsPersist: PersistenceSchema, -}); - export const WORKFLOWS_PLUGIN_NAME = "workflows"; export const WORKFLOWS_STORAGE_SERVICE_NAME = `${WORKFLOWS_PLUGIN_NAME}:storage`; -export const WORKFLOWS_PLUGIN: Plugin< - typeof WorkflowsOptionsSchema, - typeof WorkflowsSharedOptionsSchema -> = { +export const WORKFLOWS_PLUGIN: Plugin = { options: WorkflowsOptionsSchema, - sharedOptions: WorkflowsSharedOptionsSchema, bindingTypeDescription: "Workflow", async getBindings(options: z.infer) { return Object.entries(options.workflows ?? {}).map( @@ -98,12 +89,11 @@ export const WORKFLOWS_PLUGIN: Plugin< ]; }, - async getServices({ options, sharedOptions, tmpPath, defaultPersistRoot }) { + async getServices({ options, tmpPath, resourcePersistencePath }) { const persistPath = getPersistPath( WORKFLOWS_PLUGIN_NAME, tmpPath, - defaultPersistRoot, - sharedOptions.workflowsPersist + resourcePersistencePath ); await fs.mkdir(persistPath, { recursive: true }); // each workflow should get its own storage service @@ -205,13 +195,4 @@ export const WORKFLOWS_PLUGIN: Plugin< return [...storageServices, ...services]; }, - - getPersistPath({ workflowsPersist }, tmpPath) { - return getPersistPath( - WORKFLOWS_PLUGIN_NAME, - tmpPath, - undefined, - workflowsPersist - ); - }, }; diff --git a/packages/miniflare/src/runtime/index.ts b/packages/miniflare/src/runtime/index.ts index 35a9faa5296..0c35443be2d 100644 --- a/packages/miniflare/src/runtime/index.ts +++ b/packages/miniflare/src/runtime/index.ts @@ -40,7 +40,6 @@ export interface RuntimeOptions { inspectorAddress?: string; debugPortAddress?: string; verbose?: boolean; - handleRuntimeStdio?: (stdout: Readable, stderr: Readable) => void; handleStructuredLogs?: StructuredLogsHandler; // Extra environment variables to set on the spawned `workerd` subprocess. // Merged on top of `process.env` and Miniflare's own defaults @@ -88,20 +87,27 @@ function waitForExit(process: childProcess.ChildProcess): Promise { }); } -function pipeOutput(stdout: Readable, stderr: Readable) { - // TODO: may want to proxy these and prettify ✨ - // We can't just pipe() to `process.stdout/stderr` here, as Ink (used by - // wrangler), only patches the `console.*` methods: - // https://github.com/vadimdemedes/ink/blob/5d24ed8ada593a6c36ea5416f452158461e33ba5/readme.md#patchconsole - // Writing directly to `process.stdout/stderr` would result in graphical - // glitches. - // eslint-disable-next-line no-console -- Intentional console.log to forward workerd stdout through Ink-patched console - rl.createInterface(stdout).on("line", (data) => console.log(data)); - // eslint-disable-next-line no-console -- Intentional console.error to forward workerd stderr through Ink-patched console - rl.createInterface(stderr).on("line", (data) => console.error(red(data))); - // stdout.pipe(process.stdout); - // stderr.pipe(process.stderr); -} +// When no `handleStructuredLogs` handler is provided, workerd's structured logs +// are forwarded to the console by default. `warn`/`error` logs go to stderr +// (in red), everything else to stdout, matching how the raw workerd streams +// used to be forwarded. We use `console.*` rather than writing to +// `process.stdout/stderr` directly, as Ink (used by Wrangler) only patches the +// `console.*` methods: +// https://github.com/vadimdemedes/ink/blob/5d24ed8ada593a6c36ea5416f452158461e33ba5/readme.md#patchconsole +// Writing directly to `process.stdout/stderr` would result in graphical +// glitches. +const defaultStructuredLogsHandler: StructuredLogsHandler = ({ + level, + message, +}) => { + if (level === "error" || level === "warn") { + // eslint-disable-next-line no-console -- forward workerd output through Ink-patched console + console.error(red(message)); + } else { + // eslint-disable-next-line no-console -- forward workerd output through Ink-patched console + console.log(message); + } +}; function getRuntimeCommand() { return process.env.MINIFLARE_WORKERD_PATH ?? workerdPath; @@ -258,28 +264,17 @@ export class Runtime { const processExitPromise = waitForExit(runtimeProcess); this.#processExitPromise = processExitPromise; - const handleRuntimeStdio = - options.handleRuntimeStdio ?? - (options.handleStructuredLogs - ? // If `handleStructuredLogs` is provided then by default Miniflare should not pipe through the stream's output - () => {} - : pipeOutput); - - handleRuntimeStdio( - runtimeProcess.stdout.pipe(startupLogBuffer.stdoutStream), - runtimeProcess.stderr.pipe(startupLogBuffer.stderrStream) + const stdoutStream = runtimeProcess.stdout.pipe( + startupLogBuffer.stdoutStream + ); + const stderrStream = runtimeProcess.stderr.pipe( + startupLogBuffer.stderrStream ); - if (options.handleStructuredLogs) { - handleStructuredLogsFromStream( - startupLogBuffer.stdoutStream, - options.handleStructuredLogs - ); - handleStructuredLogsFromStream( - startupLogBuffer.stderrStream, - options.handleStructuredLogs - ); - } + const structuredLogsHandler = + options.handleStructuredLogs ?? defaultStructuredLogsHandler; + handleStructuredLogsFromStream(stdoutStream, structuredLogsHandler); + handleStructuredLogsFromStream(stderrStream, structuredLogsHandler); const controlPipe = runtimeProcess.stdio[3]; assert(controlPipe instanceof Readable); diff --git a/packages/miniflare/src/shared/error.ts b/packages/miniflare/src/shared/error.ts index add9fc4fcfa..91aeee011ed 100644 --- a/packages/miniflare/src/shared/error.ts +++ b/packages/miniflare/src/shared/error.ts @@ -13,15 +13,12 @@ export const USER_ERROR_CODES = new Set([ "ERR_DIFFERENT_STORAGE_BACKEND", // Multiple Durable Object bindings declared for same class with different storage backends "ERR_DIFFERENT_UNIQUE_KEYS", // Multiple Durable Object bindings declared for same class with different unsafe unique keys "ERR_DIFFERENT_PREVENT_EVICTION", // Multiple Durable Object bindings declared for same class with different unsafe prevent eviction values - "ERR_MULTIPLE_OUTBOUNDS", // Both `outboundService` and `fetchMock` specified - "ERR_INVALID_WRAPPED", // Worker not allowed to be used as wrapped binding "ERR_MISSING_INSPECTOR_PROXY_PORT", // An inspector proxy has been requested but no inspector port to use has been specified "ERR_MISSING_EXPLORER_UI", // Local Explorer enabled but assets not found at expected path ] as const); export const SYSTEM_ERROR_CODES = new Set([ "ERR_RUNTIME_FAILURE", // Runtime failed to start - "ERR_CYCLIC", // Generate cyclic workerd config "ERR_PLUGIN_LOADING_FAILED", ] as const); diff --git a/packages/miniflare/src/workers/cache/cache-entry.worker.ts b/packages/miniflare/src/workers/cache/cache-entry.worker.ts index 8112c5ef9a8..f5627ed06c6 100644 --- a/packages/miniflare/src/workers/cache/cache-entry.worker.ts +++ b/packages/miniflare/src/workers/cache/cache-entry.worker.ts @@ -1,11 +1,9 @@ import { SharedBindings } from "miniflare:shared"; -import { CacheBindings, CacheHeaders } from "./constants"; -import type { CacheObjectCf } from "./constants"; +import { CacheHeaders } from "./constants"; import type { MiniflareDurableObjectCf } from "miniflare:shared"; interface Env { [SharedBindings.DURABLE_OBJECT_NAMESPACE_OBJECT]: DurableObjectNamespace; - [CacheBindings.MAYBE_JSON_CACHE_WARN_USAGE]?: boolean; } export default >{ @@ -16,11 +14,10 @@ export default >{ const objectNamespace = env[SharedBindings.DURABLE_OBJECT_NAMESPACE_OBJECT]; const id = objectNamespace.idFromName(name); const stub = objectNamespace.get(id); - const cf: MiniflareDurableObjectCf & CacheObjectCf = { + const cf: MiniflareDurableObjectCf = { ...request.cf, miniflare: { name, - cacheWarnUsage: env[CacheBindings.MAYBE_JSON_CACHE_WARN_USAGE], }, }; return await stub.fetch(request, { cf: cf as Record }); diff --git a/packages/miniflare/src/workers/cache/cache.worker.ts b/packages/miniflare/src/workers/cache/cache.worker.ts index 95f97978dcb..431d3ece295 100644 --- a/packages/miniflare/src/workers/cache/cache.worker.ts +++ b/packages/miniflare/src/workers/cache/cache.worker.ts @@ -6,7 +6,6 @@ import { DELETE, GET, KeyValueStorage, - LogLevel, MiniflareDurableObject, parseRanges, PURGE, @@ -19,7 +18,6 @@ import { RangeNotSatisfiable, StorageFailure, } from "./errors.worker"; -import type { CacheObjectCf } from "./constants"; import type { InclusiveRange, MiniflareDurableObjectCf, @@ -36,7 +34,7 @@ interface CacheMetadata { type CacheRouteHandler = RouteHandler< unknown, - RequestInitCfProperties & MiniflareDurableObjectCf & CacheObjectCf + RequestInitCfProperties & MiniflareDurableObjectCf >; function getCacheKey(req: Request) { @@ -265,17 +263,6 @@ class SizingStream extends TransformStream { } export class CacheObject extends MiniflareDurableObject { - #warnedUsage = false; - async #maybeWarnUsage(request: Request) { - if (!this.#warnedUsage && request.cf?.miniflare?.cacheWarnUsage === true) { - this.#warnedUsage = true; - await this.logWithLevel( - LogLevel.WARN, - "Cache operations will have no impact if you deploy to a workers.dev subdomain!" - ); - } - } - #storage?: KeyValueStorage; get storage() { // `KeyValueStorage` can only be constructed once `this.blob` is initialised @@ -284,7 +271,6 @@ export class CacheObject extends MiniflareDurableObject { @GET() match: CacheRouteHandler = async (req) => { - await this.#maybeWarnUsage(req); const cacheKey = getCacheKey(req); // Never cache Workers Sites requests, so we always return on-disk files @@ -330,7 +316,6 @@ export class CacheObject extends MiniflareDurableObject { @PUT() put: CacheRouteHandler = async (req) => { - await this.#maybeWarnUsage(req); const cacheKey = getCacheKey(req); // Never cache Workers Sites requests, so we always return on-disk files @@ -384,7 +369,6 @@ export class CacheObject extends MiniflareDurableObject { @PURGE() delete: CacheRouteHandler = async (req) => { - await this.#maybeWarnUsage(req); const cacheKey = getCacheKey(req); const deleted = await this.storage.delete(cacheKey); diff --git a/packages/miniflare/src/workers/cache/constants.ts b/packages/miniflare/src/workers/cache/constants.ts index 1c807598888..1a712143748 100644 --- a/packages/miniflare/src/workers/cache/constants.ts +++ b/packages/miniflare/src/workers/cache/constants.ts @@ -2,11 +2,3 @@ export const CacheHeaders = { NAMESPACE: "cf-cache-namespace", STATUS: "cf-cache-status", } as const; - -export const CacheBindings = { - MAYBE_JSON_CACHE_WARN_USAGE: "MINIFLARE_CACHE_WARN_USAGE", -} as const; - -export interface CacheObjectCf { - miniflare?: { cacheWarnUsage?: boolean }; -} diff --git a/packages/miniflare/src/workers/shared/blob.worker.ts b/packages/miniflare/src/workers/shared/blob.worker.ts index 2e9743e63ca..c1abb57f888 100644 --- a/packages/miniflare/src/workers/shared/blob.worker.ts +++ b/packages/miniflare/src/workers/shared/blob.worker.ts @@ -182,17 +182,16 @@ export class BlobStore { readonly #fetcher: Fetcher; readonly #baseURL: string; - readonly #stickyBlobs: boolean; - constructor(fetcher: Fetcher, namespace: string, stickyBlobs: boolean) { + constructor(fetcher: Fetcher, namespace: string) { namespace = encodeURIComponent(sanitisePath(namespace)); this.#fetcher = fetcher; // `baseURL`'s `pathname` (`/${namespace}/blobs/`) is relative to the - // `*Persist` (e.g. `kvPersist`) option if defined. For example, if - // `kvPersist` is `/path/to/kv`, the `blobs` directory for a KV namespace - // with ID `TEST_NAMESPACE` would be `/path/to/kv/TEST_NAMESPACE/blobs`. + // plugin's persistence directory under `resourcePersistencePath`. For + // example, if the KV persistence directory is `/path/to/kv`, the `blobs` + // directory for a KV namespace with ID `TEST_NAMESPACE` would be + // `/path/to/kv/TEST_NAMESPACE/blobs`. this.#baseURL = `http://placeholder/${namespace}/blobs/`; - this.#stickyBlobs = stickyBlobs; } private idURL(id: BlobId) { @@ -239,8 +238,6 @@ export class BlobStore { } async delete(id: BlobId): Promise { - // If sticky blobs are enabled, don't delete any blobs - if (this.#stickyBlobs) return; // Get path for this ID and delete, ignoring if outside root or not found const idURL = this.idURL(id); if (idURL === null) return; diff --git a/packages/miniflare/src/workers/shared/constants.ts b/packages/miniflare/src/workers/shared/constants.ts index f30006a2319..ef7e61a4331 100644 --- a/packages/miniflare/src/workers/shared/constants.ts +++ b/packages/miniflare/src/workers/shared/constants.ts @@ -8,7 +8,6 @@ export const SharedBindings = { MAYBE_SERVICE_BLOBS: "MINIFLARE_BLOBS", MAYBE_SERVICE_LOOPBACK: "MINIFLARE_LOOPBACK", MAYBE_JSON_ENABLE_CONTROL_ENDPOINTS: "MINIFLARE_ENABLE_CONTROL_ENDPOINTS", - MAYBE_JSON_ENABLE_STICKY_BLOBS: "MINIFLARE_STICKY_BLOBS", } as const; export enum LogLevel { diff --git a/packages/miniflare/src/workers/shared/object.worker.ts b/packages/miniflare/src/workers/shared/object.worker.ts index c7c637a79e8..54273e243fb 100644 --- a/packages/miniflare/src/workers/shared/object.worker.ts +++ b/packages/miniflare/src/workers/shared/object.worker.ts @@ -24,11 +24,6 @@ export interface MiniflareDurableObjectEnv { // for testing. Note these endpoints allow anyone with access to the Miniflare // dev server to run arbitrary SQL queries and read arbitrary blobs. [SharedBindings.MAYBE_JSON_ENABLE_CONTROL_ENDPOINTS]?: boolean; - // If set to `true`, Miniflare won't delete blobs when deleting/overriding - // existing keys. This is a requirement for "stacked storage": when popping - // from the storage stack, we need to guarantee the blobs created in and - // before that storage stack frame still exist. - [SharedBindings.MAYBE_JSON_ENABLE_STICKY_BLOBS]?: boolean; } export interface MiniflareDurableObjectCfControlOp { @@ -78,13 +73,11 @@ export abstract class MiniflareDurableObject< get blob(): BlobStore { if (this.#blob !== undefined) return this.#blob; const maybeBlobsService = this.env[SharedBindings.MAYBE_SERVICE_BLOBS]; - const stickyBlobs = - !!this.env[SharedBindings.MAYBE_JSON_ENABLE_STICKY_BLOBS]; assert( maybeBlobsService !== undefined, `Expected ${SharedBindings.MAYBE_SERVICE_BLOBS} service binding` ); - this.#blob = new BlobStore(maybeBlobsService, this.name, stickyBlobs); + this.#blob = new BlobStore(maybeBlobsService, this.name); return this.#blob; } diff --git a/packages/miniflare/src/workers/stream/object.worker.ts b/packages/miniflare/src/workers/stream/object.worker.ts index 7e9b2647675..4a230187b87 100644 --- a/packages/miniflare/src/workers/stream/object.worker.ts +++ b/packages/miniflare/src/workers/stream/object.worker.ts @@ -21,7 +21,6 @@ const BLOB_NAMESPACE = "stream-data"; interface Env { MINIFLARE_BLOBS?: Fetcher; - MINIFLARE_STICKY_BLOBS?: boolean; [SharedBindings.MAYBE_JSON_ENABLE_CONTROL_ENDPOINTS]?: boolean; } @@ -42,12 +41,7 @@ export class StreamObject extends DurableObject { db.exec(SQL_SCHEMA); this.#db = db; this.#stmts = sqlStmts(db, () => this.#now()); - const stickyBlobs = !!env.MINIFLARE_STICKY_BLOBS; - this.#blob = new BlobStore( - env.MINIFLARE_BLOBS as Fetcher, - BLOB_NAMESPACE, - stickyBlobs - ); + this.#blob = new BlobStore(env.MINIFLARE_BLOBS as Fetcher, BLOB_NAMESPACE); } async createVideo( diff --git a/packages/miniflare/test/dev-registry.spec.ts b/packages/miniflare/test/dev-registry.spec.ts index c0976f51e72..5f7714e07f6 100644 --- a/packages/miniflare/test/dev-registry.spec.ts +++ b/packages/miniflare/test/dev-registry.spec.ts @@ -1193,7 +1193,7 @@ describe.sequential("DevRegistry", () => { serviceBindings: { remote: "remote-worker", }, - handleRuntimeStdio: () => {}, + handleStructuredLogs: () => {}, compatibilityFlags: ["experimental"], modules: true, script: ` @@ -1231,7 +1231,7 @@ describe.sequential("DevRegistry", () => { }, compatibilityFlags: ["experimental"], modules: true, - handleRuntimeStdio: () => {}, + handleStructuredLogs: () => {}, script: ` export default { async fetch(request, env) { @@ -1306,7 +1306,7 @@ describe.sequential("DevRegistry", () => { props: { tailKey: "from-tail-binding" }, }, ], - handleRuntimeStdio: () => {}, + handleStructuredLogs: () => {}, compatibilityFlags: ["experimental"], modules: true, script: ` diff --git a/packages/miniflare/test/fixtures/echo-plugin/index.ts b/packages/miniflare/test/fixtures/echo-plugin/index.ts new file mode 100644 index 00000000000..4f4db1e8ca2 --- /dev/null +++ b/packages/miniflare/test/fixtures/echo-plugin/index.ts @@ -0,0 +1,77 @@ +import { ProxyNodeBinding } from "miniflare"; +import { z } from "miniflare:zod"; +import type { Plugin, Worker_Binding } from "miniflare"; + +// Module implementing the wrapped binding. It exposes an `asyncIdentity` method +// that echoes back its arguments, allowing tests to exercise the proxy client's +// serialisation of `ReadableStream`/`Blob`/`File` arguments across the +// Node.js <-> workerd boundary. +// +// The `.pipeThrough(new TransformStream())` is required: without it we'd see +// `TypeError: Inter-TransformStream ReadableStream.pipeTo() is not implemented` +// when echoing a `ReadableStream` back. `IdentityTransformStream` doesn't work +// here. +const ECHO_MODULE_NAME = "cloudflare-internal:echo-plugin:module"; +const ECHO_MODULE = /* javascript */ ` +class Identity { + async asyncIdentity(...args) { + const i = args.findIndex((arg) => arg instanceof ReadableStream); + if (i !== -1) args[i] = args[i].pipeThrough(new TransformStream()); + return args; + } +} +export default function () { + return new Identity(); +} +`; + +export const EchoBindingOptionSchema = z.array( + z.object({ + name: z.string(), + type: z.string(), + plugin: z.object({ + package: z.string(), + name: z.string(), + }), + options: z.record(z.string(), z.unknown()), + }) +); + +export const plugins = { + "echo-plugin": { + options: EchoBindingOptionSchema, + getBindings(options) { + return options.map((binding) => ({ + name: binding.name, + wrapped: { + moduleName: ECHO_MODULE_NAME, + innerBindings: [], + }, + })); + }, + getNodeBindings(options) { + return Object.fromEntries( + options.map((binding) => [binding.name, new ProxyNodeBinding()]) + ); + }, + getServices() { + return []; + }, + getExtensions({ options }) { + if (!options.some((bindings) => bindings.length > 0)) { + return []; + } + return [ + { + modules: [ + { + name: ECHO_MODULE_NAME, + esModule: ECHO_MODULE, + internal: true, + }, + ], + }, + ]; + }, + } satisfies Plugin, +}; diff --git a/packages/miniflare/test/fixtures/unsafe-plugin/index.ts b/packages/miniflare/test/fixtures/unsafe-plugin/index.ts index d405d9b18e3..39f9cdf0c79 100644 --- a/packages/miniflare/test/fixtures/unsafe-plugin/index.ts +++ b/packages/miniflare/test/fixtures/unsafe-plugin/index.ts @@ -98,7 +98,7 @@ export const plugins = { Object.keys(options).map((name) => [name, new ProxyNodeBinding()]) ); }, - getServices({ options, unsafeStickyBlobs }) { + getServices({ options }) { if (options.length === 0) { return []; } @@ -155,7 +155,7 @@ export const plugins = { name: SharedBindings.MAYBE_SERVICE_LOOPBACK, service: { name: SERVICE_LOOPBACK }, }, - ...getMiniflareObjectBindings(unsafeStickyBlobs), + ...getMiniflareObjectBindings(), ], }, }, diff --git a/packages/miniflare/test/index.spec.ts b/packages/miniflare/test/index.spec.ts index 582fe1a86e8..d947e3bd163 100644 --- a/packages/miniflare/test/index.spec.ts +++ b/packages/miniflare/test/index.spec.ts @@ -10,19 +10,15 @@ import net from "node:net"; import os from "node:os"; import path from "node:path"; import { json, text } from "node:stream/consumers"; -import url from "node:url"; import util from "node:util"; import { _forceColour } from "@cloudflare/workers-utils"; import { _transformsForContentEncodingAndContentType, - createFetchMock, DeferredPromise, fetch, kCurrentWorker, Miniflare, MiniflareCoreError, - parseWithRootPath, - PLUGINS, Response, viewToBuffer, } from "miniflare"; @@ -51,7 +47,6 @@ import type { MiniflareOptions, ReplaceWorkersTypes, Worker_Module, - WorkerOptions, } from "miniflare"; import type { AddressInfo } from "node:net"; import type { Writable } from "node:stream"; @@ -1011,7 +1006,7 @@ test("Miniflare: service binding to named entrypoint that implements a method re test("Miniflare: tail consumer called", async ({ expect }) => { const mf = new Miniflare({ - handleRuntimeStdio: () => {}, + handleStructuredLogs: () => {}, workers: [ { name: "a", @@ -1313,51 +1308,6 @@ test("Miniflare: handles redirect responses", async ({ expect }) => { expect(await res.text()).toBe("end:https://custom.mf/external-redirected"); }); -test("Miniflare: fetch mocking", async ({ expect }) => { - const fetchMock = createFetchMock(); - fetchMock.disableNetConnect(); - const origin = fetchMock.get("https://example.com"); - origin.intercept({ method: "GET", path: "/" }).reply(200, "Mocked response!"); - - const mfOptions: MiniflareOptions = { - modules: true, - script: `export default { - async fetch() { - return fetch("https://example.com/"); - } - }`, - fetchMock, - }; - const resultOptions = {} as MiniflareOptions; - - // Verify that options with `fetchMock` can be parsed first before passing to Miniflare - // Regression test for https://github.com/cloudflare/workers-sdk/issues/5486 - for (const plugin of Object.values(PLUGINS)) { - Object.assign( - resultOptions, - parseWithRootPath("", plugin.options, mfOptions) - ); - } - - const mf = new Miniflare(resultOptions); - useDispose(mf); - const res = await mf.dispatchFetch("http://localhost"); - expect(await res.text()).toBe("Mocked response!"); - - // Check `outboundService`and `fetchMock` mutually exclusive - await expect( - mf.setOptions({ - script: "", - fetchMock, - outboundService: "", - }) - ).rejects.toThrow( - new MiniflareCoreError( - "ERR_MULTIPLE_OUTBOUNDS", - "Only one of `outboundService` or `fetchMock` may be specified per worker" - ) - ); -}); test("Miniflare: custom upstream as origin (with colons)", async ({ expect, }) => { @@ -2272,197 +2222,6 @@ test("Miniflare: getBindings() returns all bindings", async ({ ) ); }); -test("Miniflare: getBindings() returns wrapped bindings", async ({ - expect, -}) => { - const mf = new Miniflare({ - workers: [ - { - wrappedBindings: { - Greeter: { - scriptName: "greeter-implementation", - }, - }, - modules: true, - script: "", - }, - { - modules: true, - name: "greeter-implementation", - script: ` - class Greeter { - sayHello(name) { - return "Hello " + name; - } - } - - export default function (env) { - return new Greeter(); - } - `, - }, - ], - }); - useDispose(mf); - - interface Env { - Greeter: { - sayHello: (str: string) => string; - }; - } - const { Greeter } = await mf.getBindings(); - - const helloWorld = Greeter.sayHello("World"); - - expect(helloWorld).toBe("Hello World"); -}); -test("Miniflare: getBindings() handles wrapped bindings returning objects containing functions", async ({ - expect, -}) => { - const mf = new Miniflare({ - workers: [ - { - wrappedBindings: { - Greeter: { - scriptName: "greeter-obj-implementation", - }, - }, - modules: true, - script: "", - }, - { - modules: true, - name: "greeter-obj-implementation", - script: ` - export default function (env) { - const objWithFunction = { - greeting: "Hello", - sayHello(name) { - return this.greeting + ' ' + name; - } - }; - return objWithFunction; - } - `, - }, - ], - }); - useDispose(mf); - - interface Env { - Greeter: { - greeting: string; - sayHello: (str: string) => string; - }; - } - const { Greeter } = await mf.getBindings(); - - const helloWorld = Greeter.sayHello("World"); - - expect(helloWorld).toBe("Hello World"); - expect(Greeter.greeting).toBe("Hello"); -}); -test("Miniflare: getBindings() handles wrapped bindings returning objects containing nested functions", async ({ - expect, -}) => { - const mf = new Miniflare({ - workers: [ - { - wrappedBindings: { - Greeter: { - scriptName: "greeter-obj-implementation", - }, - }, - modules: true, - script: "", - }, - { - modules: true, - name: "greeter-obj-implementation", - script: ` - export default function (env) { - const objWithFunction = { - obj: { - obj1: { - obj2: { - sayHello: (name) => "Hello " + name + " from a nested function" - } - } - } - }; - return objWithFunction; - } - `, - }, - ], - }); - useDispose(mf); - - interface Env { - Greeter: { - obj: { - obj1: { - obj2: { - sayHello: (str: string) => string; - }; - }; - }; - }; - } - const { Greeter } = await mf.getBindings(); - - const helloWorld = Greeter.obj.obj1.obj2.sayHello("World"); - - expect(helloWorld).toBe("Hello World from a nested function"); -}); -test("Miniflare: getBindings() handles wrapped bindings returning functions returning functions", async ({ - expect, -}) => { - const mf = new Miniflare({ - workers: [ - { - wrappedBindings: { - GreetFactory: { - scriptName: "greet-factory-obj-implementation", - }, - }, - modules: true, - script: "", - }, - { - modules: true, - name: "greet-factory-obj-implementation", - script: ` - export default function (env) { - const factory = { - getGreetFunction(name) { - return (name) => { - return this.greeting + ' ' + name; - } - }, - greeting: "Salutations", - }; - return factory; - } - `, - }, - ], - }); - useDispose(mf); - - interface Env { - GreetFactory: { - greeting: string; - getGreetFunction: () => (str: string) => string; - }; - } - const { GreetFactory } = await mf.getBindings(); - - const greetFunction = GreetFactory.getGreetFunction(); - - expect(greetFunction("Esteemed World")).toBe("Salutations Esteemed World"); - expect(GreetFactory.greeting).toBe("Salutations"); -}); test("Miniflare: getWorker() allows dispatching events directly", async ({ expect, }) => { @@ -3021,459 +2780,6 @@ test("Miniflare: supports unsafe eval bindings", async ({ expect }) => { expect(await response.text()).toBe("the computed value is 3"); }); -test("Miniflare: supports wrapped bindings", async ({ expect }) => { - const store = new Map(); - const mf = new Miniflare({ - workers: [ - { - wrappedBindings: { - MINI_KV: { - scriptName: "mini-kv", - bindings: { NAMESPACE: "ns" }, - }, - }, - modules: true, - script: `export default { - async fetch(request, env, ctx) { - await env.MINI_KV.set("key", "value"); - const value = await env.MINI_KV.get("key"); - await env.MINI_KV.delete("key"); - const emptyValue = await env.MINI_KV.get("key"); - await env.MINI_KV.set("key", "another value"); - return Response.json({ value, emptyValue }); - } - }`, - }, - { - name: "mini-kv", - serviceBindings: { - async STORE(request) { - const { pathname } = new URL(request.url); - const key = pathname.substring(1); - if (request.method === "GET") { - const value = store.get(key); - const status = value === undefined ? 404 : 200; - return new Response(value ?? null, { status }); - } else if (request.method === "PUT") { - const value = await request.text(); - store.set(key, value); - return new Response(null, { status: 204 }); - } else if (request.method === "DELETE") { - store.delete(key); - return new Response(null, { status: 204 }); - } else { - return new Response(null, { status: 405 }); - } - }, - }, - modules: true, - script: ` - class MiniKV { - constructor(env) { - this.STORE = env.STORE; - this.baseURL = "http://x/" + (env.NAMESPACE ?? "") + ":"; - } - async get(key) { - const res = await this.STORE.fetch(this.baseURL + key); - return res.status === 404 ? null : await res.text(); - } - async set(key, body) { - await this.STORE.fetch(this.baseURL + key, { method: "PUT", body }); - } - async delete(key) { - await this.STORE.fetch(this.baseURL + key, { method: "DELETE" }); - } - } - - export default function (env) { - return new MiniKV(env); - } - `, - }, - ], - }); - useDispose(mf); - - const res = await mf.dispatchFetch("http://localhost/"); - expect(await res.json()).toEqual({ value: "value", emptyValue: null }); - expect(store).toEqual(new Map([["ns:key", "another value"]])); -}); -test("Miniflare: check overrides default bindings with bindings from wrapped binding designator", async ({ - expect, -}) => { - const mf = new Miniflare({ - workers: [ - { - wrappedBindings: { - WRAPPED: { - scriptName: "binding", - entrypoint: "wrapped", - bindings: { B: "overridden b" }, - }, - }, - modules: true, - script: `export default { - fetch(request, env, ctx) { - return env.WRAPPED(); - } - }`, - }, - { - name: "binding", - modules: true, - bindings: { A: "default a", B: "default b" }, - script: `export function wrapped(env) { - return () => Response.json(env); - }`, - }, - ], - }); - useDispose(mf); - - const res = await mf.dispatchFetch("http://localhost/"); - expect(await res.json()).toEqual({ A: "default a", B: "overridden b" }); -}); -test("Miniflare: checks uses compatibility and outbound configuration of binder", async ({ - expect, -}) => { - const workers: WorkerOptions[] = [ - { - compatibilityDate: "2022-03-21", // Default-on date for `global_navigator` - compatibilityFlags: ["nodejs_compat"], - wrappedBindings: { WRAPPED: "binding" }, - modules: true, - script: `export default { - fetch(request, env, ctx) { - return env.WRAPPED(); - } - }`, - outboundService(request) { - return new Response(`outbound:${request.url}`); - }, - }, - { - name: "binding", - modules: [ - { - type: "ESModule", - path: "index.mjs", - contents: `export default function () { - return async () => { - const typeofNavigator = typeof navigator; - let importedNode = false; - try { - await import("node:util"); - importedNode = true; - } catch {} - const outboundRes = await fetch("http://placeholder/"); - const outboundText = await outboundRes.text(); - return Response.json({ typeofNavigator, importedNode, outboundText }); - } - }`, - }, - ], - }, - ]; - const mf = new Miniflare({ workers }); - useDispose(mf); - - let res = await mf.dispatchFetch("http://localhost/"); - expect(await res.json()).toEqual({ - typeofNavigator: "object", - importedNode: true, - outboundText: "outbound:http://placeholder/", - }); - - const fetchMock = createFetchMock(); - fetchMock.disableNetConnect(); - fetchMock - .get("http://placeholder") - .intercept({ path: "/" }) - .reply(200, "mocked"); - workers[0].compatibilityDate = "2022-03-20"; - workers[0].compatibilityFlags = []; - workers[0].outboundService = undefined; - workers[0].fetchMock = fetchMock; - await mf.setOptions({ workers }); - res = await mf.dispatchFetch("http://localhost/"); - expect(await res.json()).toEqual({ - typeofNavigator: "undefined", - importedNode: false, - outboundText: "mocked", - }); -}); -test("Miniflare: cannot call getWorker() on wrapped binding worker", async ({ - expect, -}) => { - const mf = new Miniflare({ - workers: [ - { - wrappedBindings: { WRAPPED: "binding" }, - modules: true, - script: `export default { - fetch(request, env, ctx) { - return env.WRAPPED; - } - }`, - }, - { - name: "binding", - modules: true, - script: `export default function () { - return "🎁"; - }`, - }, - ], - }); - useDispose(mf); - - await expect(mf.getWorker("binding")).rejects.toThrow( - new TypeError( - '"binding" is being used as a wrapped binding, and cannot be accessed as a worker' - ) - ); -}); -test("Miniflare: prohibits invalid wrapped bindings", async ({ expect }) => { - const mf = new Miniflare({ modules: true, script: "" }); - useDispose(mf); - - // Check prohibits using entrypoint worker - await expect( - mf.setOptions({ - name: "a", - modules: true, - script: "", - wrappedBindings: { - WRAPPED: { scriptName: "a", entrypoint: "wrapped" }, - }, - }) - ).rejects.toThrow( - new MiniflareCoreError( - "ERR_INVALID_WRAPPED", - 'Cannot use "a" for wrapped binding because it\'s the entrypoint.\n' + - 'Ensure "a" isn\'t the first entry in the `workers` array.' - ) - ); - - // Check prohibits using service worker - await expect( - mf.setOptions({ - workers: [ - { modules: true, script: "", wrappedBindings: { WRAPPED: "binding" } }, - { name: "binding", script: "" }, - ], - }) - ).rejects.toThrow( - new MiniflareCoreError( - "ERR_INVALID_WRAPPED", - 'Cannot use "binding" for wrapped binding because it\'s a service worker.\n' + - 'Ensure "binding" sets `modules` to `true` or an array of modules' - ) - ); - - // Check prohibits multiple modules - await expect( - mf.setOptions({ - workers: [ - { modules: true, script: "", wrappedBindings: { WRAPPED: "binding" } }, - { - name: "binding", - modules: [ - { type: "ESModule", path: "index.mjs", contents: "" }, - { type: "ESModule", path: "dep.mjs", contents: "" }, - ], - }, - ], - }) - ).rejects.toThrow( - new MiniflareCoreError( - "ERR_INVALID_WRAPPED", - 'Cannot use "binding" for wrapped binding because it isn\'t a single module.\n' + - 'Ensure "binding" doesn\'t include unbundled `import`s.' - ) - ); - - // Check prohibits non-ES-modules - await expect( - mf.setOptions({ - workers: [ - { modules: true, script: "", wrappedBindings: { WRAPPED: "binding" } }, - { - name: "binding", - modules: [{ type: "CommonJS", path: "index.cjs", contents: "" }], - }, - ], - }) - ).rejects.toThrow( - new MiniflareCoreError( - "ERR_INVALID_WRAPPED", - 'Cannot use "binding" for wrapped binding because it isn\'t a single ES module' - ) - ); - - // Check prohibits Durable Object bindings - await expect( - mf.setOptions({ - workers: [ - { - modules: true, - script: "", - wrappedBindings: { WRAPPED: "binding" }, - durableObjects: { - OBJECT: { scriptName: "binding", className: "TestObject" }, - }, - }, - { - name: "binding", - modules: [{ type: "ESModule", path: "index.mjs", contents: "" }], - }, - ], - }) - ).rejects.toThrow( - new MiniflareCoreError( - "ERR_INVALID_WRAPPED", - 'Cannot use "binding" for wrapped binding because it is bound to with Durable Object bindings.\n' + - 'Ensure other workers don\'t define Durable Object bindings to "binding".' - ) - ); - - // Check prohibits service bindings - await expect( - mf.setOptions({ - workers: [ - { - modules: true, - script: "", - wrappedBindings: { - WRAPPED: { scriptName: "binding", entrypoint: "wrapped" }, - }, - serviceBindings: { SERVICE: "binding" }, - }, - { - name: "binding", - modules: [{ type: "ESModule", path: "index.mjs", contents: "" }], - }, - ], - }) - ).rejects.toThrow( - new MiniflareCoreError( - "ERR_INVALID_WRAPPED", - 'Cannot use "binding" for wrapped binding because it is bound to with service bindings.\n' + - 'Ensure other workers don\'t define service bindings to "binding".' - ) - ); - - // Check prohibits compatibility date and flags - await expect( - mf.setOptions({ - workers: [ - { modules: true, script: "", wrappedBindings: { WRAPPED: "binding" } }, - { - name: "binding", - compatibilityDate: "2023-11-01", - modules: [{ type: "ESModule", path: "index.mjs", contents: "" }], - }, - ], - }) - ).rejects.toThrow( - new MiniflareCoreError( - "ERR_INVALID_WRAPPED", - 'Cannot use "binding" for wrapped binding because it defines a compatibility date.\n' + - "Wrapped bindings use the compatibility date of the worker with the binding." - ) - ); - await expect( - mf.setOptions({ - workers: [ - { modules: true, script: "", wrappedBindings: { WRAPPED: "binding" } }, - { - name: "binding", - compatibilityFlags: ["nodejs_compat"], - modules: [{ type: "ESModule", path: "index.mjs", contents: "" }], - }, - ], - }) - ).rejects.toThrow( - new MiniflareCoreError( - "ERR_INVALID_WRAPPED", - 'Cannot use "binding" for wrapped binding because it defines compatibility flags.\n' + - "Wrapped bindings use the compatibility flags of the worker with the binding." - ) - ); - - // Check prohibits outbound service - await expect( - mf.setOptions({ - workers: [ - { modules: true, script: "", wrappedBindings: { WRAPPED: "binding" } }, - { - name: "binding", - outboundService() { - assert.fail(); - }, - modules: [{ type: "ESModule", path: "index.mjs", contents: "" }], - }, - ], - }) - ).rejects.toThrow( - new MiniflareCoreError( - "ERR_INVALID_WRAPPED", - 'Cannot use "binding" for wrapped binding because it defines an outbound service.\n' + - "Wrapped bindings use the outbound service of the worker with the binding." - ) - ); - - // Check prohibits cyclic wrapped bindings - await expect( - mf.setOptions({ - workers: [ - { modules: true, script: "", wrappedBindings: { WRAPPED: "binding" } }, - { - name: "binding", - wrappedBindings: { WRAPPED: "binding" }, // Simple cycle - modules: [{ type: "ESModule", path: "index.mjs", contents: "" }], - }, - ], - }) - ).rejects.toThrow( - new MiniflareCoreError( - "ERR_CYCLIC", - "Generated workerd config contains cycles. Ensure wrapped bindings don't have bindings to themselves." - ) - ); - await expect( - mf.setOptions({ - workers: [ - { - modules: true, - script: "", - wrappedBindings: { WRAPPED1: "binding-1" }, - }, - { - name: "binding-1", - wrappedBindings: { WRAPPED2: "binding-2" }, - modules: [{ type: "ESModule", path: "index.mjs", contents: "" }], - }, - { - name: "binding-2", - wrappedBindings: { WRAPPED3: "binding-3" }, - modules: [{ type: "ESModule", path: "index.mjs", contents: "" }], - }, - { - name: "binding-3", - wrappedBindings: { WRAPPED1: "binding-1" }, // Multi-step cycle - modules: [{ type: "ESModule", path: "index.mjs", contents: "" }], - }, - ], - }) - ).rejects.toThrow( - new MiniflareCoreError( - "ERR_CYCLIC", - "Generated workerd config contains cycles. Ensure wrapped bindings don't have bindings to themselves." - ) - ); -}); - test("Miniflare: getCf() returns a standard cf object", async ({ expect }) => { const mf = new Miniflare({ script: "", modules: true }); useDispose(mf); @@ -3891,7 +3197,7 @@ test("Miniflare: respects rootPath for path-valued options", async ({ await fs.writeFile(path.join(tmp, "3.txt"), "three text"); const mf = new Miniflare({ rootPath: tmp, - kvPersist: "kv", + resourcePersistencePath: tmp, workers: [ { name: "a", @@ -3956,10 +3262,10 @@ test("Miniflare: respects rootPath for path-valued options", async ({ }); expect(existsSync(path.join(tmp, "kv", "namespace"))).toBe(true); - // Check persistence URLs not resolved relative to root path + // Check persisted KV data survives an options reload await mf.setOptions({ rootPath: tmp, - kvPersist: url.pathToFileURL(path.join(tmp, "kv")).href, + resourcePersistencePath: tmp, kvNamespaces: { NAMESPACE: "namespace" }, modules: true, script: `export default { diff --git a/packages/miniflare/test/logs.spec.ts b/packages/miniflare/test/logs.spec.ts index 72bb347012b..3a449fd5c42 100644 --- a/packages/miniflare/test/logs.spec.ts +++ b/packages/miniflare/test/logs.spec.ts @@ -1,72 +1,26 @@ -import { Miniflare, MiniflareCoreError } from "miniflare"; -import { assert, test } from "vitest"; +import { Miniflare } from "miniflare"; +import { onTestFinished, test, vi } from "vitest"; import { useDispose } from "./test-shared"; import type { WorkerdStructuredLog } from "miniflare"; -test("logs are treated as standard stdout/stderr chunks by default", async ({ +test("logs are written to the console by default when no `handleStructuredLogs` is provided", async ({ expect, }) => { - const collected = { - stdout: "", - stderr: "", - }; - const mf = new Miniflare({ - modules: true, - handleRuntimeStdio(stdout, stderr) { - stdout.forEach((data) => { - collected.stdout += `${data}`; - }); - stderr.forEach((error) => { - collected.stderr += `${error}`; - }); - }, - script: ` - export default { - async fetch(req, env) { - console.log('__LOG__'); - console.warn('__WARN__'); - console.error('__ERROR__'); - console.info('__INFO__'); - console.debug('__DEBUG__'); - return new Response('Hello world!'); - } - }`, + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + onTestFinished(() => { + logSpy.mockRestore(); + errorSpy.mockRestore(); }); - useDispose(mf); - - const response = await mf.dispatchFetch("http://localhost"); - await response.text(); - expect(collected.stdout).toBe("__LOG__\n__INFO__\n__DEBUG__\n"); - expect(collected.stderr).toBe("__WARN__\n__ERROR__\n"); -}); - -test("logs are structured and all sent to stdout when `structuredWorkerdLogs` is `true`", async ({ - expect, -}) => { - const collected = { - stdout: "", - stderr: "", - }; const mf = new Miniflare({ modules: true, - structuredWorkerdLogs: true, - handleRuntimeStdio(stdout, stderr) { - stdout.forEach((data) => { - collected.stdout += `${data}`; - }); - stderr.forEach((error) => { - collected.stderr += `${error}`; - }); - }, script: ` export default { async fetch(req, env) { console.log('__LOG__'); console.warn('__WARN__'); console.error('__ERROR__'); - console.info('__INFO__'); - console.debug('__DEBUG__'); return new Response('Hello world!'); } }`, @@ -76,26 +30,16 @@ test("logs are structured and all sent to stdout when `structuredWorkerdLogs` is const response = await mf.dispatchFetch("http://localhost"); await response.text(); - expect(collected.stdout).toMatch( - /{"timestamp":\d+,"level":"log","message":"__LOG__"}/ - ); - expect(collected.stdout).toMatch( - /{"timestamp":\d+,"level":"warn","message":"__WARN__"}/ - ); - expect(collected.stdout).toMatch( - /{"timestamp":\d+,"level":"error","message":"__ERROR__"}/ - ); - expect(collected.stdout).toMatch( - /{"timestamp":\d+,"level":"info","message":"__INFO__"}/ - ); - expect(collected.stdout).toMatch( - /{"timestamp":\d+,"level":"debug","message":"__DEBUG__"}/ - ); + const stdout = logSpy.mock.calls.map((args) => args.join(" ")).join("\n"); + const stderr = errorSpy.mock.calls.map((args) => args.join(" ")).join("\n"); - expect(collected.stderr).toBe(""); + // `log` goes to stdout; `warn`/`error` go to stderr + expect(stdout).toContain("__LOG__"); + expect(stderr).toContain("__WARN__"); + expect(stderr).toContain("__ERROR__"); }); -test("logs are structured and handled via `handleStructuredLogs` when such option is provided (no `structuredWorkerdLogs: true` needed)", async ({ +test("logs are structured and handled via `handleStructuredLogs` when such option is provided", async ({ expect, }) => { const collectedLogs: (Pick & { @@ -155,77 +99,6 @@ test("logs are structured and handled via `handleStructuredLogs` when such optio ]); }); -test("even when `handleStructuredLogs` is provided, `handleRuntimeStdio` can still be used to read the raw stream values", async ({ - expect, -}) => { - let numOfCollectedStructuredLogs = 0; - const collectedRaw = { - stdout: "", - stderr: "", - }; - const mf = new Miniflare({ - modules: true, - handleRuntimeStdio(stdout, stderr) { - stdout.forEach((data) => { - collectedRaw.stdout += `${data}`; - }); - stderr.forEach((error) => { - collectedRaw.stderr += `${error}`; - }); - }, - handleStructuredLogs() { - numOfCollectedStructuredLogs++; - }, - script: ` - export default { - async fetch(req, env) { - console.log('__LOG__'); - console.error('__ERROR__'); - return new Response('Hello world!'); - } - }`, - }); - useDispose(mf); - - const response = await mf.dispatchFetch("http://localhost"); - await response.text(); - - expect(numOfCollectedStructuredLogs).toBe(2); - - expect(collectedRaw.stdout).toMatch( - /{"timestamp":\d+,"level":"log","message":"__LOG__"}/ - ); - expect(collectedRaw.stdout).toMatch( - /{"timestamp":\d+,"level":"error","message":"__ERROR__"}/ - ); - expect(collectedRaw.stderr).toBe(""); -}); - -test("setting `handleStructuredLogs` when `structuredWorkerdLogs` is `false` triggers an error", async ({ - expect, -}) => { - const mf = new Miniflare({ modules: true, script: "" }); - useDispose(mf); - - let error: MiniflareCoreError | undefined = undefined; - try { - new Miniflare({ - modules: true, - script: "", - structuredWorkerdLogs: false, - handleStructuredLogs() {}, - }); - } catch (e) { - error = e as MiniflareCoreError; - } - - assert(error instanceof MiniflareCoreError); - expect(error.code).toBe("ERR_VALIDATION"); - expect(error.message).toContain( - "A `handleStructuredLogs` has been provided but `structuredWorkerdLogs` is set to `false`" - ); -}); - test("when using `handleStructuredLogs` some known unhelpful logs are filtered out (e.g. CODE_MOVED warnings)", async ({ expect, }) => { diff --git a/packages/miniflare/test/plugins/cache/index.spec.ts b/packages/miniflare/test/plugins/cache/index.spec.ts index 61189d36e72..c510b9ebf3a 100644 --- a/packages/miniflare/test/plugins/cache/index.spec.ts +++ b/packages/miniflare/test/plugins/cache/index.spec.ts @@ -1,13 +1,8 @@ import assert from "node:assert"; import crypto from "node:crypto"; import fs from "node:fs/promises"; -import { - CACHE_PLUGIN_NAME, - LogLevel, - Miniflare, - Request, - Response, -} from "miniflare"; +import path from "node:path"; +import { CACHE_PLUGIN_NAME, Miniflare, Request, Response } from "miniflare"; import { beforeEach, type ExpectStatic, onTestFinished, test } from "vitest"; import { MiniflareDurableObjectControlStub, @@ -419,39 +414,13 @@ test("operations respect cf.cacheKey", async ({ expect }) => { const deleted2 = await cache.delete(key2); expect(deleted2).toBe(true); }); -test("operations log warning on workers.dev subdomain", async ({ expect }) => { - // Set option, then reset after test - await ctx.setOptions({ cacheWarnUsage: true }); - onTestFinished(() => ctx.setOptions({})); - ctx.caches = await ctx.mf.getCaches(); - const defaultObject = await getControlStub(ctx.mf); - - const cache = ctx.caches.default; - const key = "http://localhost/cache-workers-dev-warning"; - - ctx.log.logs = []; - const resToCache = new Response("body", { - headers: { "Cache-Control": "max-age=3600" }, - }); - await cache.put(key, resToCache.clone()); - await defaultObject.waitForFakeTasks(); - expect(ctx.log.logsAtLevel(LogLevel.WARN)).toEqual([ - "Cache operations will have no impact if you deploy to a workers.dev subdomain!", - ]); - - // Check only warns once - ctx.log.logs = []; - await cache.put(key, resToCache); - await defaultObject.waitForFakeTasks(); - expect(ctx.log.logsAtLevel(LogLevel.WARN)).toEqual([]); -}); test("operations persist cached data", async ({ expect }) => { // Create new temporary file-system persistence directory const tmp = await useTmp(); const opts: MiniflareOptions = { modules: true, script: "", - cachePersist: tmp, + resourcePersistencePath: tmp, }; let mf = new Miniflare(opts); useDispose(mf); @@ -465,8 +434,8 @@ test("operations persist cached data", async ({ expect }) => { }); await cache.put(key, resToCache); - // Check directory created for namespace - const names = await fs.readdir(tmp); + // Check directory created for namespace under the plugin subdirectory + const names = await fs.readdir(path.join(tmp, CACHE_PLUGIN_NAME)); expect(names.includes("miniflare-CacheObject")).toBe(true); // Check "restarting" keeps persisted data @@ -486,7 +455,7 @@ test("operations persist cached data", async ({ expect }) => { }); test("operations are no-ops when caching disabled", async ({ expect }) => { // Set option, then reset after test - await ctx.setOptions({ cache: false }); + await ctx.setOptions({ cacheAPI: false }); onTestFinished(() => ctx.setOptions({})); ctx.caches = await ctx.mf.getCaches(); diff --git a/packages/miniflare/test/plugins/core/inspector-proxy/index.spec.ts b/packages/miniflare/test/plugins/core/inspector-proxy/index.spec.ts index 1a0fba7a40a..66181034f66 100644 --- a/packages/miniflare/test/plugins/core/inspector-proxy/index.spec.ts +++ b/packages/miniflare/test/plugins/core/inspector-proxy/index.spec.ts @@ -706,12 +706,10 @@ test("InspectorProxy: can proxy messages > 1MB", async ({ expect }) => { const mf = new Miniflare({ inspectorPort: 0, - // Avoid the default handling of stdio since that will console log the very large string in the test output. - handleRuntimeStdio(stdout, stderr) { - // We need to add these handlers otherwise the streams will not be consumed and the process will hang. - stdout.on("data", () => {}); - stderr.on("data", () => {}); - }, + // Avoid the default handling of stdio since that will console log the very + // large string in the test output. A no-op structured log handler consumes + // the runtime output without forwarding it to the console. + handleStructuredLogs() {}, workers: [ { script: ` diff --git a/packages/miniflare/test/plugins/core/proxy/client.spec.ts b/packages/miniflare/test/plugins/core/proxy/client.spec.ts index 01627a92759..b4f80928a2f 100644 --- a/packages/miniflare/test/plugins/core/proxy/client.spec.ts +++ b/packages/miniflare/test/plugins/core/proxy/client.spec.ts @@ -1,6 +1,7 @@ import assert from "node:assert"; import { Blob } from "node:buffer"; import http from "node:http"; +import path from "node:path"; import { text } from "node:stream/consumers"; import { ReadableStream, WritableStream } from "node:stream/web"; import util from "node:util"; @@ -13,7 +14,7 @@ import { WebSocketPair, } from "miniflare"; import { describe, onTestFinished, test } from "vitest"; -import { useDispose } from "../../../test-shared"; +import { EXPORTED_FIXTURES, useDispose } from "../../../test-shared"; import type { Fetcher } from "@cloudflare/workers-types/experimental"; import type { MessageEvent, ReplaceWorkersTypes } from "miniflare"; @@ -81,38 +82,29 @@ describe("ProxyClient", () => { test("supports serialising multiple ReadableStreams, Blobs and Files", async ({ expect, }) => { - // For testing proxy client serialisation, add an API that just returns its - // arguments. Note without the `.pipeThrough(new TransformStream())` below, - // we'll see `TypeError: Inter-TransformStream ReadableStream.pipeTo() is - // not implemented.`. `IdentityTransformStream` doesn't work here. + // For testing proxy client serialisation, use a wrapped binding inside an + // unsafe binding. This is just an echo module that just returns its arguments + // (see the `echo-plugin` fixture). + const echoPlugin = path.resolve(EXPORTED_FIXTURES, "echo-plugin/index.js"); const mf = new Miniflare({ - workers: [ - { - name: "entry", - modules: true, - script: "", - wrappedBindings: { IDENTITY: "identity" }, - }, + name: "entry", + modules: true, + script: "", + unsafeBindings: [ { - name: "identity", - modules: true, - script: ` - class Identity { - async asyncIdentity(...args) { - const i = args.findIndex((arg) => arg instanceof ReadableStream); - if (i !== -1) args[i] = args[i].pipeThrough(new TransformStream()); - return args; - } - } - export default function() { return new Identity(); } - `, + name: "IDENTITY", + type: "wrapped", + plugin: { package: echoPlugin, name: "echo-plugin" }, + options: {}, }, ], }); useDispose(mf); const client = await mf._getProxyClient(); - const IDENTITY = client.env["MINIFLARE_PROXY:core:entry:IDENTITY"] as { + const IDENTITY = client.env[ + "MINIFLARE_PROXY:echo-plugin:entry:IDENTITY" + ] as { asyncIdentity(...args: Args): Promise; }; @@ -151,6 +143,7 @@ describe("ProxyClient", () => { expect(allResult[2].lastModified).toBe(1000); expect(await allResult[2].text()).toBe("text file"); }); + test("poisons dependent proxies after setOptions()/dispose()", async ({ expect, }) => { diff --git a/packages/miniflare/test/plugins/d1/suite.ts b/packages/miniflare/test/plugins/d1/suite.ts index 8b3cc171c8d..60c20a69e79 100644 --- a/packages/miniflare/test/plugins/d1/suite.ts +++ b/packages/miniflare/test/plugins/d1/suite.ts @@ -1,7 +1,8 @@ import assert from "node:assert"; import fs from "node:fs/promises"; +import path from "node:path"; import { type D1Database } from "@cloudflare/workers-types/experimental"; -import { Miniflare } from "miniflare"; +import { D1_PLUGIN_NAME, Miniflare } from "miniflare"; import { beforeEach, type ExpectStatic, onTestFinished, test } from "vitest"; import { useDispose, useTmp, utf8Encode } from "../../test-shared"; import { binding, ctx, getDatabase, opts } from "./test"; @@ -507,7 +508,10 @@ test("operations persist D1 data", async ({ expect }) => { // Create new temporary file-system persistence directory const tmp = await useTmp(); - const persistOpts: MiniflareOptions = { ...opts, d1Persist: tmp }; + const persistOpts: MiniflareOptions = { + ...opts, + resourcePersistencePath: tmp, + }; const mf = new Miniflare(persistOpts); useDispose(mf); let db = await getDatabase(mf); @@ -524,8 +528,8 @@ test("operations persist D1 data", async ({ expect }) => { .first(); expect(result).toEqual({ name: "purple" }); - // Check directory created for database - const names = await fs.readdir(tmp); + // Check directory created for database under the plugin subdirectory + const names = await fs.readdir(path.join(tmp, D1_PLUGIN_NAME)); expect(names.includes("miniflare-D1DatabaseObject")).toBe(true); // Check "restarting" keeps persisted data @@ -599,7 +603,7 @@ test("dumpSql exports and imports complete database structure and content correc const tmp1 = await useTmp(); const originalMF = new Miniflare({ ...opts, - d1Persist: tmp1, + resourcePersistencePath: tmp1, d1Databases: { test: "test" }, }); useDispose(originalMF); @@ -620,7 +624,7 @@ test("dumpSql exports and imports complete database structure and content correc const tmp2 = await useTmp(); const mirrorMF = new Miniflare({ ...opts, - d1Persist: tmp2, + resourcePersistencePath: tmp2, d1Databases: { test: "test" }, }); useDispose(mirrorMF); diff --git a/packages/miniflare/test/plugins/do/index.spec.ts b/packages/miniflare/test/plugins/do/index.spec.ts index 049b06e7d9d..e396c6ae586 100644 --- a/packages/miniflare/test/plugins/do/index.spec.ts +++ b/packages/miniflare/test/plugins/do/index.spec.ts @@ -5,6 +5,7 @@ import { setTimeout } from "node:timers/promises"; import { removeDir } from "@cloudflare/workers-utils"; import { DeferredPromise, + DURABLE_OBJECTS_PLUGIN_NAME, kUnsafeEphemeralUniqueKey, Miniflare, } from "miniflare"; @@ -71,20 +72,17 @@ test("persists Durable Object data in-memory between options reloads", async ({ res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("Options #2: 2"); - opts.durableObjectsPersist = false; opts.script = COUNTER_SCRIPT("Options #3: "); await mf.setOptions(opts); res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("Options #3: 3"); - opts.durableObjectsPersist = "memory:"; opts.script = COUNTER_SCRIPT("Options #4: "); await mf.setOptions(opts); res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("Options #4: 4"); // Check a `new Miniflare()` instance has its own in-memory storage - delete opts.durableObjectsPersist; opts.script = COUNTER_SCRIPT("Options #5: "); await mf.dispose(); const mf2 = new Miniflare(opts); @@ -112,7 +110,7 @@ test("persists Durable Object data on file-system", async ({ expect }) => { modules: true, script: COUNTER_SCRIPT(), durableObjects: { COUNTER: "Counter" }, - durableObjectsPersist: tmp, + resourcePersistencePath: tmp, }; const mf = new Miniflare(opts); useDispose(mf); @@ -120,8 +118,9 @@ test("persists Durable Object data on file-system", async ({ expect }) => { let res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("1"); - // Check directory created for "worker"'s Durable Object - const names = await fs.readdir(tmp); + // Check directory created for "worker"'s Durable Object under the plugin subdirectory + const doTmp = path.join(tmp, DURABLE_OBJECTS_PLUGIN_NAME); + const names = await fs.readdir(doTmp); expect(names).toEqual(["worker-Counter"]); // Check reloading keeps persisted data @@ -133,7 +132,7 @@ test("persists Durable Object data on file-system", async ({ expect }) => { // reload here as `workerd` keeps a copy of the SQLite database in-memory, // we also need to `dispose()` to avoid `EBUSY` error on Windows) await mf.dispose(); - await removeDir(path.join(tmp, names[0])); + await removeDir(path.join(doTmp, names[0])); const mf2 = new Miniflare(opts); useDispose(mf2); @@ -152,7 +151,7 @@ test("persists Durable Object data on file-system", async ({ expect }) => { test("lists Durable Object ids with persisted storage", async ({ expect }) => { const tmp = await useTmp(); const mf = new Miniflare({ - defaultPersistRoot: tmp, + resourcePersistencePath: tmp, name: "worker", modules: true, script: COUNTER_SCRIPT(), @@ -182,7 +181,7 @@ test("lists Durable Object ids with persisted storage", async ({ expect }) => { test("multiple Workers access same Durable Object data", async ({ expect }) => { const tmp = await useTmp(); const mf = new Miniflare({ - durableObjectsPersist: tmp, + resourcePersistencePath: tmp, workers: [ { name: "entry", @@ -234,8 +233,8 @@ test("multiple Workers access same Durable Object data", async ({ expect }) => { }); expect(await res.text()).toBe("via A: b: 1"); - // Check directory created for Durable Objects - const names = await fs.readdir(tmp); + // Check directory created for Durable Objects under the plugin subdirectory + const names = await fs.readdir(path.join(tmp, DURABLE_OBJECTS_PLUGIN_NAME)); expect(names.sort()).toEqual(["a-Counter", "b-Counter"]); // Check accessing via a different service accesses same persisted data diff --git a/packages/miniflare/test/plugins/email/index.spec.ts b/packages/miniflare/test/plugins/email/index.spec.ts index 6f11cc48cff..0f3dda90933 100644 --- a/packages/miniflare/test/plugins/email/index.spec.ts +++ b/packages/miniflare/test/plugins/email/index.spec.ts @@ -63,7 +63,7 @@ test("Unbound send_email binding works", async ({ expect }) => { email: { send_email: [{ name: "SEND_EMAIL" }], }, - defaultProjectTmpPath: projectTmpPath, + resourceTmpPath: projectTmpPath, compatibilityDate: "2025-03-17", }); @@ -163,7 +163,7 @@ test("Single allowed destination send_email binding works", async ({ { name: "SEND_EMAIL", destination_address: "someone-else@example.com" }, ], }, - defaultProjectTmpPath: projectTmpPath, + resourceTmpPath: projectTmpPath, compatibilityDate: "2025-03-17", }); @@ -1079,7 +1079,7 @@ test("MessageBuilder with text only", async ({ expect }) => { email: { send_email: [{ name: "SEND_EMAIL" }], }, - defaultProjectTmpPath: projectTmpPath, + resourceTmpPath: projectTmpPath, compatibilityDate: "2025-03-17", }); @@ -1194,7 +1194,7 @@ test("MessageBuilder with attachments", async ({ expect }) => { email: { send_email: [{ name: "SEND_EMAIL" }], }, - defaultProjectTmpPath: projectTmpPath, + resourceTmpPath: projectTmpPath, compatibilityDate: "2025-03-17", }); @@ -1260,7 +1260,7 @@ test("MessageBuilder log output format snapshot", async ({ expect }) => { email: { send_email: [{ name: "SEND_EMAIL" }], }, - defaultProjectTmpPath: projectTmpPath, + resourceTmpPath: projectTmpPath, compatibilityDate: "2025-03-17", }); @@ -2083,7 +2083,7 @@ test("disposing does not remove a concurrent email session", async ({ email: { send_email: [{ name: "SEND_EMAIL" }], }, - defaultProjectTmpPath: projectTmpPath, + resourceTmpPath: projectTmpPath, compatibilityDate: "2025-03-17", }); @@ -2123,7 +2123,7 @@ describe("EMAIL_PLUGIN.getServices", () => { }, sharedOptions: {}, tmpPath: tmp, - defaultProjectTmpPath: projectTmpPath, + resourceTmpPath: projectTmpPath, workerNames: ["default"], workerIndex: 0, } as unknown as Parameters[0]); @@ -2206,7 +2206,7 @@ describe("EMAIL_PLUGIN.getServices", () => { expect(emailDiskServices[1].path).toBe(projectDisk.disk.path); }); - test("creates only system disk service when defaultProjectTmpPath is undefined", async ({ + test("creates only system disk service when resourceTmpPath is undefined", async ({ expect, }) => { const tmp = await useTmp(); @@ -2217,7 +2217,7 @@ describe("EMAIL_PLUGIN.getServices", () => { }, sharedOptions: {}, tmpPath: tmp, - defaultProjectTmpPath: undefined, + resourceTmpPath: undefined, workerNames: ["default"], workerIndex: 0, } as unknown as Parameters[0]); @@ -2307,7 +2307,7 @@ describe("getEmailPathsToClean", () => { }); }); -test("MessageBuilder writes files to system temp when defaultProjectTmpPath is unset", async ({ +test("MessageBuilder writes files to system temp when resourceTmpPath is unset", async ({ expect, }) => { const log = new TestLog(); diff --git a/packages/miniflare/test/plugins/hello-world/index.spec.ts b/packages/miniflare/test/plugins/hello-world/index.spec.ts index 5e6995beb0b..c9f0d90503d 100644 --- a/packages/miniflare/test/plugins/hello-world/index.spec.ts +++ b/packages/miniflare/test/plugins/hello-world/index.spec.ts @@ -10,7 +10,6 @@ test("hello-world", async ({ expect }) => { enable_timer: true, }, }, - helloWorldPersist: false, modules: true, script: ` export default { diff --git a/packages/miniflare/test/plugins/images/index.spec.ts b/packages/miniflare/test/plugins/images/index.spec.ts index 2a210502850..ecd37841438 100644 --- a/packages/miniflare/test/plugins/images/index.spec.ts +++ b/packages/miniflare/test/plugins/images/index.spec.ts @@ -54,7 +54,6 @@ function createMiniflare(): Miniflare { return new Miniflare({ compatibilityDate: "2025-04-01", images: { binding: "IMAGES" }, - imagesPersist: false, modules: true, script: WORKER_SCRIPT, } satisfies MiniflareOptions); diff --git a/packages/miniflare/test/plugins/kv/index.spec.ts b/packages/miniflare/test/plugins/kv/index.spec.ts index de91b1741ac..c595402abb5 100644 --- a/packages/miniflare/test/plugins/kv/index.spec.ts +++ b/packages/miniflare/test/plugins/kv/index.spec.ts @@ -1,7 +1,7 @@ import assert from "node:assert"; import { Blob } from "node:buffer"; import fs from "node:fs/promises"; -import consumers from "node:stream/consumers"; +import path from "node:path"; import { KV_PLUGIN_NAME, MAX_BULK_GET_KEYS, Miniflare } from "miniflare"; import { beforeEach, type ExpectStatic, test } from "vitest"; import { @@ -788,7 +788,7 @@ test("persists on file-system", async ({ expect }) => { modules: true, script: "", kvNamespaces: { NAMESPACE: "namespace" }, - kvPersist: tmp, + resourcePersistencePath: tmp, }; let mf = new Miniflare(opts); useDispose(mf); @@ -797,8 +797,8 @@ test("persists on file-system", async ({ expect }) => { await kv.put("key", "value"); expect(await kv.get("key")).toBe("value"); - // Check directory created for namespace - const names = await fs.readdir(tmp); + // Check directory created for namespace under the plugin subdirectory + const names = await fs.readdir(path.join(tmp, KV_PLUGIN_NAME)); expect(names.includes("miniflare-KVNamespaceObject")).toBe(true); // Check "restarting" keeps persisted data @@ -808,43 +808,3 @@ test("persists on file-system", async ({ expect }) => { kv = await mf.getKVNamespace("NAMESPACE"); expect(await kv.get("key")).toBe("value"); }); - -test("sticky blobs never deleted", async ({ expect }) => { - // Checking regular behaviour that old blobs deleted in `put: overrides - // existing keys` test. Only testing sticky blobs for KV, as the blob store - // should only be constructed in the shared `MiniflareDurableObject` ABC. - - // Create instance with sticky blobs enabled (can't use `ctx.mf`) - const mf = new Miniflare({ - script: "", - modules: true, - kvNamespaces: ["NAMESPACE"], - unsafeStickyBlobs: true, - }); - useDispose(mf); - - // Create control stub for newly created instance's namespace - const objectNamespace = await mf._getInternalDurableObjectNamespace( - KV_PLUGIN_NAME, - "kv:ns", - "KVNamespaceObject" - ); - const objectId = objectNamespace.idFromName("NAMESPACE"); - const objectStub = objectNamespace.get(objectId); - const object = new MiniflareDurableObjectControlStub(objectStub); - await object.enableFakeTimers(secondsToMillis(TIME_NOW)); - const stmts = sqlStmts(object); - - // Store something in the namespace and get the blob ID - const ns = await mf.getKVNamespace("NAMESPACE"); - await ns.put("key", "value 1"); - const blobId = await stmts.getBlobIdByKey("key"); - assert(blobId !== undefined); - - // Override key and check we can still access the old blob - await ns.put("key", "value 2"); - await object.waitForFakeTasks(); - const blob = await object.getBlob(blobId); - assert(blob !== null); - expect(await consumers.text(blob)).toBe("value 1"); -}); diff --git a/packages/miniflare/test/plugins/local-explorer/aggregation.spec.ts b/packages/miniflare/test/plugins/local-explorer/aggregation.spec.ts index 1034f276a7f..32c677a819f 100644 --- a/packages/miniflare/test/plugins/local-explorer/aggregation.spec.ts +++ b/packages/miniflare/test/plugins/local-explorer/aggregation.spec.ts @@ -633,7 +633,7 @@ describe("Same ID across multiple instances with same persistence directories", script: `export default { fetch() { return new Response("Worker A"); } }`, unsafeLocalExplorer: true, unsafeDevRegistryPath: registryPath, - defaultPersistRoot: persistencePath, + resourcePersistencePath: persistencePath, kvNamespaces: { MY_KV: "shared-kv-id", }, @@ -652,7 +652,7 @@ describe("Same ID across multiple instances with same persistence directories", script: `export default { fetch() { return new Response("Worker B"); } }`, unsafeLocalExplorer: true, unsafeDevRegistryPath: registryPath, - defaultPersistRoot: persistencePath, + resourcePersistencePath: persistencePath, kvNamespaces: { MY_KV: "shared-kv-id", }, diff --git a/packages/miniflare/test/plugins/local-explorer/do.spec.ts b/packages/miniflare/test/plugins/local-explorer/do.spec.ts index d28897ac181..940dea834f3 100644 --- a/packages/miniflare/test/plugins/local-explorer/do.spec.ts +++ b/packages/miniflare/test/plugins/local-explorer/do.spec.ts @@ -188,7 +188,7 @@ describe("Durable Objects API", () => { } `, unsafeLocalExplorer: true, - defaultPersistRoot: persistPath, + resourcePersistencePath: persistPath, durableObjects: { TEST_DO: { className: "TestDO", useSQLite: true }, }, diff --git a/packages/miniflare/test/plugins/r2/index.spec.ts b/packages/miniflare/test/plugins/r2/index.spec.ts index c48ff14dcf6..7ca639a6585 100644 --- a/packages/miniflare/test/plugins/r2/index.spec.ts +++ b/packages/miniflare/test/plugins/r2/index.spec.ts @@ -3,6 +3,7 @@ import assert from "node:assert"; import crypto from "node:crypto"; import fs from "node:fs/promises"; +import path from "node:path"; import { text } from "node:stream/consumers"; import { Headers, Miniflare, R2_PLUGIN_NAME } from "miniflare"; import { beforeEach, type ExpectStatic, onTestFinished, test } from "vitest"; @@ -998,7 +999,7 @@ test("operations persist stored data", async ({ expect }) => { modules: true, script: "", r2Buckets: { BUCKET: "bucket" }, - r2Persist: tmp, + resourcePersistencePath: tmp, }; const mf = new Miniflare(persistOpts); useDispose(mf); @@ -1011,8 +1012,8 @@ test("operations persist stored data", async ({ expect }) => { let object = await r2.head("key"); expect(object?.size).toBe(5); - // Check directory created for namespace - const names = await fs.readdir(tmp); + // Check directory created for namespace under the plugin subdirectory + const names = await fs.readdir(path.join(tmp, R2_PLUGIN_NAME)); expect(names.includes("miniflare-R2BucketObject")).toBe(true); // Check "restarting" keeps persisted data diff --git a/packages/miniflare/test/plugins/secret-store/index.spec.ts b/packages/miniflare/test/plugins/secret-store/index.spec.ts index 25de329f69e..50dda2f052b 100644 --- a/packages/miniflare/test/plugins/secret-store/index.spec.ts +++ b/packages/miniflare/test/plugins/secret-store/index.spec.ts @@ -11,7 +11,6 @@ test("single secret-store", async ({ expect }) => { secret_name: "secret_name", }, }, - secretsStorePersist: false, modules: true, script: ` export default { diff --git a/packages/miniflare/test/plugins/stream/index.spec.ts b/packages/miniflare/test/plugins/stream/index.spec.ts index 202c2a94dbc..e47bc902fda 100644 --- a/packages/miniflare/test/plugins/stream/index.spec.ts +++ b/packages/miniflare/test/plugins/stream/index.spec.ts @@ -1,4 +1,3 @@ -import { pathToFileURL } from "node:url"; import { Miniflare, STREAM_COMPAT_DATE, @@ -140,7 +139,6 @@ function createMiniflare(options: Partial = {}): Miniflare { return new Miniflare({ compatibilityDate: STREAM_COMPAT_DATE, stream: { binding: "STREAM" }, - streamPersist: false, modules: true, script: WORKER_SCRIPT, ...options, @@ -706,7 +704,6 @@ describe("Stream reloads", () => { const opts = { compatibilityDate: STREAM_COMPAT_DATE, stream: { binding: "STREAM" }, - streamPersist: false, modules: true, script: WORKER_SCRIPT, } satisfies MiniflareOptions; @@ -732,9 +729,7 @@ describe("Stream reloads", () => { expect(videos[0].id).toBe(video.id); }); - test("keeps persisted data when persistence path format changes on reload", async ({ - expect, - }) => { + test("keeps persisted data across setOptions reloads", async ({ expect }) => { const tmp = await useTmp(); const { http: videoUrl } = await useServer( staticBytesListener(TEST_VIDEO_BYTES) @@ -742,7 +737,7 @@ describe("Stream reloads", () => { const opts = { compatibilityDate: STREAM_COMPAT_DATE, stream: { binding: "STREAM" }, - streamPersist: tmp, + resourcePersistencePath: tmp, modules: true, script: WORKER_SCRIPT, } satisfies MiniflareOptions; @@ -755,7 +750,6 @@ describe("Stream reloads", () => { await mf.setOptions({ ...opts, - streamPersist: pathToFileURL(tmp).href, script: `${WORKER_SCRIPT}\n// reload persisted stream worker`, }); diff --git a/packages/miniflare/test/plugins/workflows/index.spec.ts b/packages/miniflare/test/plugins/workflows/index.spec.ts index 56061c1fb7d..9ab62d25b6d 100644 --- a/packages/miniflare/test/plugins/workflows/index.spec.ts +++ b/packages/miniflare/test/plugins/workflows/index.spec.ts @@ -1,6 +1,7 @@ import * as fs from "node:fs/promises"; +import path from "node:path"; import { scheduler } from "node:timers/promises"; -import { Miniflare } from "miniflare"; +import { Miniflare, WORKFLOWS_PLUGIN_NAME } from "miniflare"; import { describe, test } from "vitest"; import { useDispose, useTmp } from "../../test-shared"; import type { MiniflareOptions } from "miniflare"; @@ -42,7 +43,7 @@ test("starts Workflows with user-provided experimental compatibility flag", asyn ], }, }, - workflowsPersist: tmp, + resourcePersistencePath: tmp, }); useDispose(mf); @@ -67,7 +68,7 @@ test("persists Workflow data on file-system between runs", async ({ name: "MY_WORKFLOW", }, }, - workflowsPersist: tmp, + resourcePersistencePath: tmp, }; const mf = new Miniflare(opts); useDispose(mf); @@ -97,8 +98,8 @@ test("persists Workflow data on file-system between runs", async ({ true ); - // check if files were committed - const names = await fs.readdir(tmp); + // check if files were committed under the plugin subdirectory + const names = await fs.readdir(path.join(tmp, WORKFLOWS_PLUGIN_NAME)); expect(names).toEqual(["miniflare-workflows-MY_WORKFLOW"]); // restart miniflare @@ -191,7 +192,7 @@ function lifecycleMiniflareOpts(tmp: string): MiniflareOptions { name: "LIFECYCLE_WORKFLOW", }, }, - workflowsPersist: tmp, + resourcePersistencePath: tmp, }; } diff --git a/packages/vite-plugin-cloudflare/src/miniflare-options.ts b/packages/vite-plugin-cloudflare/src/miniflare-options.ts index 5b9eef7644a..44b98d44c65 100644 --- a/packages/vite-plugin-cloudflare/src/miniflare-options.ts +++ b/packages/vite-plugin-cloudflare/src/miniflare-options.ts @@ -251,6 +251,7 @@ export async function getDevMiniflareOptions( ]; const containerTagToOptionsMap: ContainerTagToOptionsMap = new Map(); + let containerEngine: string | undefined; const workersFromConfig = resolvedPluginConfig.type === "workers" @@ -293,8 +294,7 @@ export async function getDevMiniflareOptions( worker.config.dev.enable_containers ) { const dockerPath = getDockerPath(); - worker.config.dev.container_engine = - resolveDockerHost(dockerPath); + containerEngine = resolveDockerHost(dockerPath); containerBuildId = generateContainerBuildId(); const options = getContainerOptions({ @@ -474,14 +474,12 @@ export async function getDevMiniflareOptions( unsafeLocalExplorer: getLocalExplorerEnabledFromEnv(), telemetry: { enabled: false }, handleStructuredLogs: getStructuredLogsLogger(logger), - defaultPersistRoot: getPersistenceRoot( + resourcePersistencePath: getPersistenceRoot( resolvedViteConfig.root, resolvedPluginConfig.persistState ), - defaultProjectTmpPath: path.resolve( - resolvedViteConfig.root, - ".wrangler/tmp" - ), + resourceTmpPath: path.resolve(resolvedViteConfig.root, ".wrangler/tmp"), + containerEngine, workers: [...assetWorkers, ...externalWorkers, ...userWorkers], async unsafeModuleFallbackService(request) { const parsed = await parseModuleFallbackRequest(request); @@ -648,6 +646,7 @@ export async function getPreviewMiniflareOptions( ); const { resolvedPluginConfig, resolvedViteConfig } = ctx; const containerTagToOptionsMap: ContainerTagToOptionsMap = new Map(); + let containerEngine: string | undefined; const workers: Array = ( await Promise.all( @@ -688,7 +687,7 @@ export async function getPreviewMiniflareOptions( workerConfig.dev.enable_containers ) { const dockerPath = getDockerPath(); - workerConfig.dev.container_engine = resolveDockerHost(dockerPath); + containerEngine = resolveDockerHost(dockerPath); containerBuildId = generateContainerBuildId(); const options = getContainerOptions({ @@ -758,14 +757,12 @@ export async function getPreviewMiniflareOptions( unsafeLocalExplorer: getLocalExplorerEnabledFromEnv(), telemetry: { enabled: false }, handleStructuredLogs: getStructuredLogsLogger(logger), - defaultPersistRoot: getPersistenceRoot( + resourcePersistencePath: getPersistenceRoot( resolvedViteConfig.root, resolvedPluginConfig.persistState ), - defaultProjectTmpPath: path.resolve( - resolvedViteConfig.root, - ".wrangler/tmp" - ), + resourceTmpPath: path.resolve(resolvedViteConfig.root, ".wrangler/tmp"), + containerEngine, workers, }, containerTagToOptionsMap, diff --git a/packages/vitest-pool-workers/src/pool/index.ts b/packages/vitest-pool-workers/src/pool/index.ts index 26edf2485d8..7d34883a574 100644 --- a/packages/vitest-pool-workers/src/pool/index.ts +++ b/packages/vitest-pool-workers/src/pool/index.ts @@ -626,7 +626,6 @@ const SHARED_MINIFLARE_OPTIONS: SharedOptions = { log: mfLog, verbose: true, handleStructuredLogs, - unsafeStickyBlobs: true, } satisfies Partial; const DEFAULT_INSPECTOR_PORT = 9229; diff --git a/packages/wrangler/src/api/integrations/platform/index.ts b/packages/wrangler/src/api/integrations/platform/index.ts index cb35d7e6551..522193935c9 100644 --- a/packages/wrangler/src/api/integrations/platform/index.ts +++ b/packages/wrangler/src/api/integrations/platform/index.ts @@ -1,8 +1,6 @@ import path from "node:path"; -import { resolveDockerHost } from "@cloudflare/containers-shared"; import { extractBindingsOfType } from "@cloudflare/deploy-helpers"; import { - getDockerPath, getRegistryPath, getTodaysCompatDate, } from "@cloudflare/workers-utils"; @@ -321,11 +319,11 @@ async function getMiniflareOptionsFromConfig(args: { ? buildAssetOptions({ assets: processedAssetOptions }) : {}; - const defaultPersistRoot = getMiniflarePersistRoot(options.persist); + const resourcePersistencePath = getMiniflarePersistRoot(options.persist); const projectRoot = config.userConfigPath ? path.dirname(config.userConfigPath) : process.cwd(); - const defaultProjectTmpPath = getDefaultProjectTmpPath(projectRoot); + const resourceTmpPath = getDefaultProjectTmpPath(projectRoot); const miniflareOptions: MiniflareOptions = { workers: [ @@ -339,8 +337,8 @@ async function getMiniflareOptionsFromConfig(args: { }, ...externalWorkers, ], - defaultPersistRoot, - defaultProjectTmpPath, + resourcePersistencePath, + resourceTmpPath, }; return { @@ -502,15 +500,10 @@ export function unstable_getMiniflareWorkerOptions( ? buildAssetOptions({ assets: processedAssetOptions }) : {}; - const useContainers = - config.dev?.enable_containers && config.containers?.length; const workerOptions: SourcelessWorkerOptions = { compatibilityDate: config.compatibility_date, compatibilityFlags: config.compatibility_flags, modulesRules, - containerEngine: useContainers - ? (config.dev.container_engine ?? resolveDockerHost(getDockerPath())) - : undefined, zone: getZoneFromConfig(config), ...bindingOptions, diff --git a/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts b/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts index a38e11118b5..4e1c4bd18ef 100644 --- a/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts +++ b/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts @@ -199,8 +199,6 @@ export async function convertToConfigBundle( queueConsumers, outboundService: event.config.dev.outboundService, localProtocol: event.config.dev?.server?.secure ? "https" : "http", - httpsCertPath: event.config.dev?.server?.httpsCertPath, - httpsKeyPath: event.config.dev?.server?.httpsKeyPath, localUpstream: event.config.dev?.origin?.hostname, upstreamProtocol: event.config.dev?.origin?.secure ? "https" : "http", testScheduled: !!event.config.dev.testScheduled, diff --git a/packages/wrangler/src/api/startDevWorker/ProxyController.ts b/packages/wrangler/src/api/startDevWorker/ProxyController.ts index 1bfcc3a449f..a5fee81f145 100644 --- a/packages/wrangler/src/api/startDevWorker/ProxyController.ts +++ b/packages/wrangler/src/api/startDevWorker/ProxyController.ts @@ -113,7 +113,7 @@ export class ProxyController extends Controller { }, // no need to use file-system, so don't - cache: false, + cacheAPI: false, unsafeEphemeralDurableObjects: true, }, ], @@ -172,7 +172,7 @@ export class ProxyController extends Controller { }, ], // no need to use file-system, so don't - cache: false, + cacheAPI: false, unsafeEphemeralDurableObjects: true, }); } diff --git a/packages/wrangler/src/d1/execute.ts b/packages/wrangler/src/d1/execute.ts index 327824bf793..290a0a64fbb 100644 --- a/packages/wrangler/src/d1/execute.ts +++ b/packages/wrangler/src/d1/execute.ts @@ -317,7 +317,8 @@ async function executeLocally({ // TODO(#11870): Really we should prefer localDB.name here, but that would break users with existing local databases. const id = localDB.previewDatabaseUuid ?? localDB.uuid ?? localDB.binding; const persistencePath = getLocalPersistencePath(persistTo, config); - const d1Persist = path.join(persistencePath, "v3", "d1"); + const resourcePersistencePath = path.join(persistencePath, "v3"); + const d1Persist = path.join(resourcePersistencePath, "d1"); logger.log( `🌀 Executing on local database ${name} (${id}) from ${readableRelative( @@ -331,7 +332,7 @@ async function executeLocally({ const mf = new Miniflare({ modules: true, script: "", - d1Persist, + resourcePersistencePath, d1Databases: { DATABASE: id }, }); const db = await mf.getD1Database("DATABASE"); diff --git a/packages/wrangler/src/d1/export.ts b/packages/wrangler/src/d1/export.ts index b6e6ae71788..43617f0c348 100644 --- a/packages/wrangler/src/d1/export.ts +++ b/packages/wrangler/src/d1/export.ts @@ -152,7 +152,8 @@ async function exportLocal( // TODO: should we allow customising persistence path? // Should it be --persist-to for consistency (even though this isn't persisting anything)? const persistencePath = getLocalPersistencePath(undefined, config); - const d1Persist = path.join(persistencePath, "v3", "d1"); + const resourcePersistencePath = path.join(persistencePath, "v3"); + const d1Persist = path.join(resourcePersistencePath, "d1"); logger.log( `🌀 Exporting local database ${name} (${id}) from ${readableRelative( @@ -166,7 +167,7 @@ async function exportLocal( const mf = new Miniflare({ modules: true, script: "export default {}", - d1Persist, + resourcePersistencePath, d1Databases: { DATABASE: id }, }); const db = await mf.getD1Database("DATABASE"); diff --git a/packages/wrangler/src/dev/miniflare/index.ts b/packages/wrangler/src/dev/miniflare/index.ts index 50bf3e46cd5..d2c8964f6b2 100644 --- a/packages/wrangler/src/dev/miniflare/index.ts +++ b/packages/wrangler/src/dev/miniflare/index.ts @@ -91,8 +91,6 @@ export interface ConfigBundle { routes: string[] | undefined; queueConsumers: Config["queues"]["consumers"]; localProtocol: "http" | "https"; - httpsKeyPath: string | undefined; - httpsCertPath: string | undefined; localUpstream: string | undefined; upstreamProtocol: "http" | "https"; inspect: boolean; @@ -465,7 +463,6 @@ type WorkerOptionsBindings = Pick< | "serviceBindings" | "ratelimits" | "workflows" - | "wrappedBindings" | "secretsStoreSecrets" | "images" | "email" @@ -695,8 +692,6 @@ export function buildMiniflareBindingOptions( const externalWorkers: WorkerOptions[] = []; - const wrappedBindings: WorkerOptions["wrappedBindings"] = {}; - for (const ai of aiBindings) { warnOrError("ai", ai.remote); } @@ -1084,7 +1079,6 @@ export function buildMiniflareBindingOptions( }) ), serviceBindings, - wrappedBindings: wrappedBindings, tails, streamingTails, }; @@ -1158,8 +1152,10 @@ export async function buildMiniflareOptions( bindingOptions.browserRendering.headful = true; } const sitesOptions = buildSitesOptions(config); - const defaultPersistRoot = getDefaultPersistRoot(config.localPersistencePath); - const defaultProjectTmpPath = getDefaultProjectTmpPath(config.projectRoot); + const resourcePersistencePath = getDefaultPersistRoot( + config.localPersistencePath + ); + const resourceTmpPath = getDefaultProjectTmpPath(config.projectRoot); const assetOptions = buildAssetOptions(config); const options: MiniflareOptions = { @@ -1186,8 +1182,9 @@ export async function buildMiniflareOptions( log, verbose: logger.loggerLevel === "debug", handleStructuredLogs: config.structuredLogsHandler ?? handleStructuredLogs, - defaultPersistRoot, - defaultProjectTmpPath, + resourcePersistencePath, + resourceTmpPath, + containerEngine: config.containerEngine, workers: [ { name: getName(config), @@ -1200,7 +1197,6 @@ export async function buildMiniflareOptions( ...assetOptions, routes: config.routes, outboundService: config.outboundService, - containerEngine: config.containerEngine, zone: config.zone, }, ...externalWorkers, diff --git a/packages/wrangler/src/hello-world/index.ts b/packages/wrangler/src/hello-world/index.ts index 3a6f8934412..9145ad0c1e1 100644 --- a/packages/wrangler/src/hello-world/index.ts +++ b/packages/wrangler/src/hello-world/index.ts @@ -23,11 +23,11 @@ export async function usingLocalHelloWorldBinding( ) => Promise ): Promise { const persist = getLocalPersistencePath(persistTo, config); - const defaultPersistRoot = getDefaultPersistRoot(persist); + const resourcePersistencePath = getDefaultPersistRoot(persist); const mf = new Miniflare({ script: 'addEventListener("fetch", (e) => e.respondWith(new Response(null, { status: 404 })))', - defaultPersistRoot, + resourcePersistencePath, helloWorld: { BINDING: { enable_timer: false, diff --git a/packages/wrangler/src/kv/helpers.ts b/packages/wrangler/src/kv/helpers.ts index cd01bff4b3c..f07ba894375 100644 --- a/packages/wrangler/src/kv/helpers.ts +++ b/packages/wrangler/src/kv/helpers.ts @@ -584,11 +584,11 @@ export async function usingLocalNamespace( // We need to cast to Config for the getLocalPersistencePath function since // it expects a full Config object, even though it only uses compliance_region const persist = getLocalPersistencePath(persistTo, config); - const defaultPersistRoot = getDefaultPersistRoot(persist); + const resourcePersistencePath = getDefaultPersistRoot(persist); const mf = new Miniflare({ script: 'addEventListener("fetch", (e) => e.respondWith(new Response(null, { status: 404 })))', - defaultPersistRoot, + resourcePersistencePath, kvNamespaces: { NAMESPACE: namespaceId }, }); const namespace = await mf.getKVNamespace("NAMESPACE"); diff --git a/packages/wrangler/src/r2/helpers/object.ts b/packages/wrangler/src/r2/helpers/object.ts index 9563b60ed97..3a169ed0fd2 100644 --- a/packages/wrangler/src/r2/helpers/object.ts +++ b/packages/wrangler/src/r2/helpers/object.ts @@ -157,7 +157,7 @@ export async function usingLocalBucket( ) => Promise ): Promise { const persist = getLocalPersistencePath(persistTo, config); - const defaultPersistRoot = getDefaultPersistRoot(persist); + const resourcePersistencePath = getDefaultPersistRoot(persist); const mf = new Miniflare({ modules: true, // TODO(soon): import `reduceError()` from `miniflare:shared` @@ -189,7 +189,7 @@ export async function usingLocalBucket( } } }`, - defaultPersistRoot, + resourcePersistencePath, r2Buckets: { BUCKET: bucketName }, }); const bucket = await mf.getR2Bucket("BUCKET"); diff --git a/packages/wrangler/src/secrets-store/commands.ts b/packages/wrangler/src/secrets-store/commands.ts index 040e4cf5022..aeabc4796f3 100644 --- a/packages/wrangler/src/secrets-store/commands.ts +++ b/packages/wrangler/src/secrets-store/commands.ts @@ -34,11 +34,11 @@ export async function usingLocalSecretsStoreSecretAPI( ) => Promise ): Promise { const persist = getLocalPersistencePath(persistTo, config); - const defaultPersistRoot = getDefaultPersistRoot(persist); + const resourcePersistencePath = getDefaultPersistRoot(persist); const mf = new Miniflare({ script: 'addEventListener("fetch", (e) => e.respondWith(new Response(null, { status: 404 })))', - defaultPersistRoot, + resourcePersistencePath, secretsStoreSecrets: { SECRET: { store_id: storeId, From b825807a1545d9ff6527d2bf422d94be71b412d2 Mon Sep 17 00:00:00 2001 From: emily-shen Date: Tue, 21 Jul 2026 15:19:14 +0100 Subject: [PATCH 5/6] update changeset --- .changeset/consolidate-miniflare-persist-options.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.changeset/consolidate-miniflare-persist-options.md b/.changeset/consolidate-miniflare-persist-options.md index 00c2d817366..b822197f473 100644 --- a/.changeset/consolidate-miniflare-persist-options.md +++ b/.changeset/consolidate-miniflare-persist-options.md @@ -4,7 +4,11 @@ Consolidate persistence and temporary directory options -The per-resource persistence options (`kvPersist`, `r2Persist`, `d1Persist`, `cachePersist`, `durableObjectsPersist`, `workflowsPersist`, `secretsStorePersist`, `analyticsEngineDatasetsPersist`, `streamPersist`, `imagesPersist`, and `helloWorldPersist`) have been removed, along with `defaultPersistRoot` and `defaultProjectTmpPath`. Persistence is now configured with two options: +The per-resource persistence options (`kvPersist`, `r2Persist`, `d1Persist`, `cachePersist`, `durableObjectsPersist`, `workflowsPersist`, `secretsStorePersist`, `analyticsEngineDatasetsPersist`, `streamPersist`, `imagesPersist`, and `helloWorldPersist`) have been removed. The `Miniflare.unsafeGetPersistPaths()` method, which provided the per-resource persistence paths, has also been removed as they can now be stably inferred from the base path. + +For consistency and clarity, `defaultPersistRoot` and `defaultProjectTmpPath` have been renamed to `resourcePersistencePath` and `resourceTmpPath`, respectively. + +For example: ```js new Miniflare({ @@ -13,4 +17,4 @@ new Miniflare({ }); ``` -When `resourcePersistencePath` is set, each resource persists to a subdirectory named after its plugin (e.g. `.wrangler/state/v3/kv`). When it is omitted, resources are ephemeral and their data is cleared on dispose. The `boolean`, `memory:`, `file:` and relative-path forms of the old per-resource options, the `.mf` default directory, and the `Miniflare.unsafeGetPersistPaths()` method have also been removed. +When `resourcePersistencePath` is set, each resource persists to a subdirectory named after its plugin (e.g. `.wrangler/state/v3/kv`). When it is omitted, resources are ephemeral and their data is cleared on dispose. From b231e274ebb04dd4c30568d0970c1c40b9930465 Mon Sep 17 00:00:00 2001 From: Edmund Hung Date: Wed, 29 Jul 2026 15:58:12 +0100 Subject: [PATCH 6/6] [vitest-pool-workers] Preserve deprecated cache option --- .changeset/quiet-caches-rest.md | 7 ++ .../vitest-pool-workers/src/pool/config.ts | 18 ++++- .../test/validation.test.ts | 67 +++++++++++++++++++ 3 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 .changeset/quiet-caches-rest.md diff --git a/.changeset/quiet-caches-rest.md b/.changeset/quiet-caches-rest.md new file mode 100644 index 00000000000..855b272f3d0 --- /dev/null +++ b/.changeset/quiet-caches-rest.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/vitest-pool-workers": patch +--- + +Preserve the deprecated Miniflare `cache` option + +Vitest configurations using `cache` continue to work after the internal Miniflare v5 upgrade. The option is translated to `cacheAPI`; new configurations should use `cacheAPI` directly. diff --git a/packages/vitest-pool-workers/src/pool/config.ts b/packages/vitest-pool-workers/src/pool/config.ts index a16cd6fdfbf..1b9e70a3b19 100644 --- a/packages/vitest-pool-workers/src/pool/config.ts +++ b/packages/vitest-pool-workers/src/pool/config.ts @@ -72,8 +72,13 @@ const WorkersPoolOptionsSchema = z.object({ .optional(), }); +type CompatibleWorkerOptions = WorkerOptions & { + /** @deprecated Use `cacheAPI` instead. */ + cache?: WorkerOptions["cacheAPI"]; +}; + export type SourcelessWorkerOptions = Omit< - WorkerOptions, + CompatibleWorkerOptions, "script" | "scriptPath" | "modules" | "modulesRoot" > & { // `modulesRules` is not included in all members of the `SourceOptions` type @@ -83,7 +88,7 @@ export type SourcelessWorkerOptions = Omit< export type WorkersPoolOptions = z.input & { miniflare?: SourcelessWorkerOptions & { - workers?: WorkerOptions[]; + workers?: CompatibleWorkerOptions[]; }; }; @@ -93,6 +98,13 @@ export type WorkersPoolOptionsWithDefines = WorkersPoolOptions & { type PathParseParams = { path?: (string | number)[] }; +function normalizeMiniflareWorkerOptions(value: Record): void { + if (value.cacheAPI === undefined) { + value.cacheAPI = value.cache; + } + delete value.cache; +} + function isZodErrorLike(value: unknown): value is ZodError { return ( typeof value === "object" && @@ -120,6 +132,8 @@ function parseWorkerOptions( withoutScript: boolean, opts: PathParseParams ): WorkerOptions { + normalizeMiniflareWorkerOptions(value); + // If this worker shouldn't have a configurable script, remove all script data // and replace it with an empty `script` that will pass validation if (withoutScript) { diff --git a/packages/vitest-pool-workers/test/validation.test.ts b/packages/vitest-pool-workers/test/validation.test.ts index 203d3fc63f1..3d3e1c19655 100644 --- a/packages/vitest-pool-workers/test/validation.test.ts +++ b/packages/vitest-pool-workers/test/validation.test.ts @@ -54,6 +54,73 @@ test( } ); +test( + "normalizes the deprecated cache option", + { timeout: 45_000 }, + async ({ expect, seed, vitestRun }) => { + await seed({ + "vitest.config.mts": vitestConfig({ + miniflare: { + cache: false, + compatibilityDate: "2025-12-02", + compatibilityFlags: ["nodejs_compat"], + }, + }), + "index.test.ts": dedent /* javascript */ ` + import { it } from "vitest"; + + it("disables the cache", async ({ expect }) => { + const key = "https://example.com/cache"; + await caches.default.put( + key, + new Response("cached", { + headers: { "Cache-Control": "max-age=3600" }, + }) + ); + expect(await caches.default.match(key)).toBeUndefined(); + }); + `, + }); + + const result = await vitestRun(); + expect(await result.exitCode, result.stderr).toBe(0); + } +); + +test( + "gives cacheAPI precedence over the deprecated cache option", + { timeout: 45_000 }, + async ({ expect, seed, vitestRun }) => { + await seed({ + "vitest.config.mts": vitestConfig({ + miniflare: { + cache: true, + cacheAPI: false, + compatibilityDate: "2025-12-02", + compatibilityFlags: ["nodejs_compat"], + }, + }), + "index.test.ts": dedent /* javascript */ ` + import { it } from "vitest"; + + it("disables the cache", async ({ expect }) => { + const key = "https://example.com/cache"; + await caches.default.put( + key, + new Response("cached", { + headers: { "Cache-Control": "max-age=3600" }, + }) + ); + expect(await caches.default.match(key)).toBeUndefined(); + }); + `, + }); + + const result = await vitestRun(); + expect(await result.exitCode, result.stderr).toBe(0); + } +); + test( "requires modules entrypoint to use SELF", { timeout: 45_000 },