diff --git a/.changeset/consolidate-miniflare-persist-options.md b/.changeset/consolidate-miniflare-persist-options.md new file mode 100644 index 00000000000..b822197f473 --- /dev/null +++ b/.changeset/consolidate-miniflare-persist-options.md @@ -0,0 +1,20 @@ +--- +"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. 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({ + 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. 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-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}); @@ -597,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}`; } @@ -683,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, @@ -761,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({ @@ -778,7 +660,6 @@ export const CORE_PLUGIN: Plugin< sharedOptions, workerBindings, workerIndex, - wrappedBindingNames, durableObjectClassNames, additionalModules, loopbackHost, @@ -851,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, @@ -972,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) { @@ -1072,7 +897,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 +912,6 @@ export function getGlobalServices({ sharedOptions, allWorkerRoutes, fallbackWorkerName, - loopbackPort, tmpPath, log, proxyBindings, @@ -1203,14 +1026,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/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 007f73d68eb..b4bb8c53f75 100644 --- a/packages/miniflare/src/plugins/d1/index.ts +++ b/packages/miniflare/src/plugins/d1/index.ts @@ -8,11 +8,9 @@ import { getMiniflareObjectBindings, getPersistPath, getUserBindingServiceName, - migrateDatabase, namespaceEntries, namespaceKeys, objectEntryWorker, - PersistenceSchema, ProxyNodeBinding, remoteProxyClientWorker, SERVICE_LOOPBACK, @@ -28,6 +26,7 @@ export const D1OptionsSchema = z.object({ d1Databases: z .union([ z.record( + z.string(), z.union([ z.string(), z.object({ @@ -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,15 +103,7 @@ export const D1_PLUGIN: Plugin< databases.map((name) => [name, new ProxyNodeBinding()]) ); }, - async getServices({ - options, - sharedOptions, - tmpPath, - defaultPersistRoot, - log, - unsafeStickyBlobs, - }) { - const persist = sharedOptions.d1Persist; + async getServices({ options, tmpPath, resourcePersistencePath }) { const databases = namespaceEntries(options.d1Databases); const services: Service[] = []; @@ -148,8 +131,7 @@ export const D1_PLUGIN: Plugin< const persistPath = getPersistPath( D1_PLUGIN_NAME, tmpPath, - defaultPersistRoot, - persist + resourcePersistencePath ); await fs.mkdir(persistPath, { recursive: true }); @@ -186,23 +168,13 @@ export const D1_PLUGIN: Plugin< name: SharedBindings.MAYBE_SERVICE_LOOPBACK, service: { name: SERVICE_LOOPBACK }, }, - ...getMiniflareObjectBindings(unsafeStickyBlobs), + ...getMiniflareObjectBindings(), ], }, }; services.push(storageService, objectService); - - for (const [, database] of databases) { - if (database.remoteProxyConnectionString) { - continue; - } - await migrateDatabase(log, uniqueKey, persistPath, database.id); - } } return services; }, - getPersistPath({ d1Persist }, tmpPath) { - return getPersistPath(D1_PLUGIN_NAME, tmpPath, undefined, d1Persist); - }, }; 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..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"; @@ -29,7 +28,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,14 +44,13 @@ 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(), }); -export const DurableObjectsSharedOptionsSchema = z.object({ - durableObjectsPersist: PersistenceSchema, -}); export function normaliseDurableObject( designator: NonNullable< @@ -108,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( @@ -132,9 +133,8 @@ export const DURABLE_OBJECTS_PLUGIN: Plugin< ); }, async getServices({ - sharedOptions, tmpPath, - defaultPersistRoot, + resourcePersistencePath, durableObjectClassNames, unsafeEphemeralDurableObjects, }) { @@ -157,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 @@ -174,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/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..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"; @@ -18,6 +17,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(), }) @@ -25,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) { @@ -65,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) { @@ -81,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 }); @@ -119,7 +104,7 @@ export const HELLO_WORLD_PLUGIN: Plugin< name: SharedBindings.MAYBE_SERVICE_LOOPBACK, service: { name: SERVICE_LOOPBACK }, }, - ...getMiniflareObjectBindings(unsafeStickyBlobs), + ...getMiniflareObjectBindings(), ], }, } satisfies Service; @@ -151,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/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/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 ff79ca4100c..6da8d3a1da6 100644 --- a/packages/miniflare/src/plugins/kv/index.ts +++ b/packages/miniflare/src/plugins/kv/index.ts @@ -7,11 +7,9 @@ import { buildRemoteProxyProps, getMiniflareObjectBindings, getPersistPath, - migrateDatabase, namespaceEntries, namespaceKeys, objectEntryWorker, - PersistenceSchema, ProxyNodeBinding, remoteProxyClientWorker, SERVICE_LOOPBACK, @@ -34,6 +32,7 @@ export const KVOptionsSchema = z.object({ kvNamespaces: z .union([ z.record( + z.string(), z.union([ z.string(), z.object({ @@ -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,15 +126,7 @@ export const KV_PLUGIN: Plugin< return bindings; }, - async getServices({ - options, - sharedOptions, - tmpPath, - defaultPersistRoot, - log, - unsafeStickyBlobs, - }) { - const persist = sharedOptions.kvPersist; + async getServices({ options, tmpPath, resourcePersistencePath }) { const namespaces = namespaceEntries(options.kvNamespaces); const services: Service[] = []; @@ -175,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 = { @@ -209,23 +191,11 @@ export const KV_PLUGIN: Plugin< name: SharedBindings.MAYBE_SERVICE_LOOPBACK, service: { name: SERVICE_LOOPBACK }, }, - ...getMiniflareObjectBindings(unsafeStickyBlobs), + ...getMiniflareObjectBindings(), ], }, }; 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)) { @@ -234,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/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..959f46d3452 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(), }); @@ -75,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 @@ -117,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 59543f5d409..20e0b69190a 100644 --- a/packages/miniflare/src/plugins/r2/index.ts +++ b/packages/miniflare/src/plugins/r2/index.ts @@ -8,11 +8,9 @@ import { getMiniflareObjectBindings, getPersistPath, getUserBindingServiceName, - migrateDatabase, namespaceEntries, namespaceKeys, objectEntryWorker, - PersistenceSchema, ProxyNodeBinding, remoteProxyClientWorker, SERVICE_LOOPBACK, @@ -28,6 +26,7 @@ export const R2OptionsSchema = z.object({ r2Buckets: z .union([ z.record( + z.string(), z.union([ z.string(), z.object({ @@ -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,15 +113,7 @@ export const R2_PLUGIN: Plugin< buckets.map((name) => [name, new ProxyNodeBinding()]) ); }, - async getServices({ - options, - sharedOptions, - tmpPath, - defaultPersistRoot, - log, - unsafeStickyBlobs, - }) { - const persist = sharedOptions.r2Persist; + async getServices({ options, tmpPath, resourcePersistencePath }) { const buckets = namespaceEntries(options.r2Buckets); const services: Service[] = []; @@ -158,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 = { @@ -195,23 +177,13 @@ export const R2_PLUGIN: Plugin< name: SharedBindings.MAYBE_SERVICE_LOOPBACK, service: { name: SERVICE_LOOPBACK }, }, - ...getMiniflareObjectBindings(unsafeStickyBlobs), + ...getMiniflareObjectBindings(), ], }, }; services.push(storageService, objectService); - - for (const [, bucket] of buckets) { - if (bucket.remoteProxyConnectionString) { - continue; - } - await migrateDatabase(log, uniqueKey, persistPath, bucket.id); - } } 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 e7a81765d64..c42ee653f25 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"; @@ -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 609c6e8a811..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"; @@ -17,6 +16,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(), @@ -27,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) { @@ -72,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) : []; @@ -90,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 }); @@ -129,7 +116,7 @@ export const SECRET_STORE_PLUGIN: Plugin< name: SharedBindings.MAYBE_SERVICE_LOOPBACK, service: { name: SERVICE_LOOPBACK }, }, - ...getMiniflareObjectBindings(unsafeStickyBlobs), + ...getMiniflareObjectBindings(), ], }, } satisfies Service; @@ -177,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 baf5928461c..4d61caa0a55 100644 --- a/packages/miniflare/src/plugins/shared/index.ts +++ b/packages/miniflare/src/plugins/shared/index.ts @@ -1,11 +1,7 @@ -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 { pathToFileURL } from "node:url"; +import { MiniflareCoreError } from "../../shared"; import type { Extension, Service, @@ -21,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.string().url()` and `PathSchema` - // will result in a `string`, but `PathSchema` gets resolved relative to the - // closest `rootPath`. - .union([z.boolean(), z.string().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 { @@ -77,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; @@ -120,10 +101,6 @@ export interface PluginBase< getServices( options: PluginServicesOptions ): Awaitable; - getPersistPath?( - sharedOptions: OptionalZodTypeOf, - tmpPath: string - ): string; getExtensions?(options: { options: z.infer[]; }): Awaitable; @@ -143,7 +120,7 @@ export type Plugin< */ export async function loadExternalPlugins( packageName: string -): Promise>> { +): Promise>> { let pluginModule; try { const pluginPath = require.resolve(packageName); @@ -223,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); @@ -233,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 @@ -284,62 +231,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/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/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..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"; @@ -16,6 +15,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(), @@ -36,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( @@ -97,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 @@ -204,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 7bd790cd3ec..0c35443be2d 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"; @@ -42,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 @@ -90,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; @@ -260,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); @@ -385,21 +378,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/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/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/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/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/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/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/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/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/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/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/queues/schemas.ts b/packages/miniflare/src/workers/queues/schemas.ts index d714a273ce1..458d8f8f8de 100644 --- a/packages/miniflare/src/workers/queues/schemas.ts +++ b/packages/miniflare/src/workers/queues/schemas.ts @@ -19,26 +19,17 @@ 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 - // 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.string().optional(), + retryDelay: QueueMessageDelaySchema, +}); export const QueueConsumerSchema = /* @__PURE__ */ z.intersection( QueueConsumerOptionsSchema, z.object({ workerName: z.string() }) @@ -49,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"]) @@ -64,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/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/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/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/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/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/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 cb44f6282aa..00000000000 Binary files a/packages/miniflare/test/fixtures/migrations/3.20230821.0/d1/DATABASE/db.sqlite and /dev/null differ diff --git a/packages/miniflare/test/fixtures/migrations/3.20230821.0/kv/NAMESPACE/blobs/c708857cd996a02e1c61a4d5af45519baf21ce5c1a39c5baea36f40e912e81e6000637434f5e7544 b/packages/miniflare/test/fixtures/migrations/3.20230821.0/kv/NAMESPACE/blobs/c708857cd996a02e1c61a4d5af45519baf21ce5c1a39c5baea36f40e912e81e6000637434f5e7544 deleted file mode 100644 index 2890eead23d..00000000000 --- a/packages/miniflare/test/fixtures/migrations/3.20230821.0/kv/NAMESPACE/blobs/c708857cd996a02e1c61a4d5af45519baf21ce5c1a39c5baea36f40e912e81e6000637434f5e7544 +++ /dev/null @@ -1 +0,0 @@ -value \ No newline at end of file diff --git a/packages/miniflare/test/fixtures/migrations/3.20230821.0/kv/NAMESPACE/db.sqlite b/packages/miniflare/test/fixtures/migrations/3.20230821.0/kv/NAMESPACE/db.sqlite deleted file mode 100644 index cee2a8e1971..00000000000 Binary files a/packages/miniflare/test/fixtures/migrations/3.20230821.0/kv/NAMESPACE/db.sqlite and /dev/null differ diff --git a/packages/miniflare/test/fixtures/migrations/3.20230821.0/r2/BUCKET/blobs/776759739fb970e463921f00822d78ae8455152966feb09ac09ec3cc4b27dcff000637435043236c b/packages/miniflare/test/fixtures/migrations/3.20230821.0/r2/BUCKET/blobs/776759739fb970e463921f00822d78ae8455152966feb09ac09ec3cc4b27dcff000637435043236c deleted file mode 100644 index 2890eead23d..00000000000 --- a/packages/miniflare/test/fixtures/migrations/3.20230821.0/r2/BUCKET/blobs/776759739fb970e463921f00822d78ae8455152966feb09ac09ec3cc4b27dcff000637435043236c +++ /dev/null @@ -1 +0,0 @@ -value \ No newline at end of file diff --git a/packages/miniflare/test/fixtures/migrations/3.20230821.0/r2/BUCKET/db.sqlite b/packages/miniflare/test/fixtures/migrations/3.20230821.0/r2/BUCKET/db.sqlite deleted file mode 100644 index a54e7e97ad8..00000000000 Binary files a/packages/miniflare/test/fixtures/migrations/3.20230821.0/r2/BUCKET/db.sqlite and /dev/null differ 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 22372d107a8..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 { - _forceColour, _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"; @@ -123,7 +118,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 +136,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` ); }); @@ -208,13 +203,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 +218,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 +232,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(); @@ -1024,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", @@ -1326,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, }) => { @@ -1724,11 +1661,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 +1732,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 +1742,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 +1782,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 +1829,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 +1878,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 +1924,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 +1987,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 +2007,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 +2023,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 +2085,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) => { @@ -2287,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, }) => { @@ -3036,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); @@ -3906,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", @@ -3971,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/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/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 e89eca93e3e..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", }); @@ -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", @@ -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 178d2e3f215..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); @@ -93,7 +92,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 +102,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 +114,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 +129,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/kv/index.spec.ts b/packages/miniflare/test/plugins/kv/index.spec.ts index bb16aed11ce..c595402abb5 100644 --- a/packages/miniflare/test/plugins/kv/index.spec.ts +++ b/packages/miniflare/test/plugins/kv/index.spec.ts @@ -2,12 +2,10 @@ 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, @@ -790,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); @@ -799,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 @@ -810,63 +808,3 @@ test("persists on file-system", async ({ expect }) => { kv = await mf.getKVNamespace("NAMESPACE"); 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 - // 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/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 2b492d4f8d7..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", }, ], }); @@ -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/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/plugins/r2/index.spec.ts b/packages/miniflare/test/plugins/r2/index.spec.ts index 077f1b6f1b3..7ca639a6585 100644 --- a/packages/miniflare/test/plugins/r2/index.spec.ts +++ b/packages/miniflare/test/plugins/r2/index.spec.ts @@ -8,7 +8,6 @@ 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, @@ -1000,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); @@ -1013,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 @@ -1608,24 +1607,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/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 33ed4a1a9aa..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, @@ -211,7 +209,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 +644,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 +657,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 +669,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); @@ -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`, }); @@ -1626,7 +1620,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 +1641,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/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/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 3c9c0e27d67..00000000000 Binary files a/packages/miniflare/test/snapshots/zod-format.spec.ts.snap and /dev/null differ diff --git a/packages/miniflare/test/zod-format.spec.ts b/packages/miniflare/test/zod-format.spec.ts deleted file mode 100644 index d3ebfb0bec1..00000000000 --- a/packages/miniflare/test/zod-format.spec.ts +++ /dev/null @@ -1,458 +0,0 @@ -import assert from "node:assert"; -import { _forceColour, formatZodError } from "miniflare"; -import { describe, test } from "vitest"; -import { z } from "zod"; - -function formatZodErrorForTest( - schema: z.ZodTypeAny, - 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 - ^ 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/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/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..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,7 +1,7 @@ import { readFileSync } from "node:fs"; -import path from "node:path"; +import { join } 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, @@ -70,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/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/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/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/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..1b9e70a3b19 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,18 +60,25 @@ 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(), }); +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 @@ -81,7 +88,7 @@ export type SourcelessWorkerOptions = Omit< export type WorkersPoolOptions = z.input & { miniflare?: SourcelessWorkerOptions & { - workers?: WorkerOptions[]; + workers?: CompatibleWorkerOptions[]; }; }; @@ -89,7 +96,14 @@ export type WorkersPoolOptionsWithDefines = WorkersPoolOptions & { defines?: Record; }; -type PathParseParams = Pick; +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 ( @@ -118,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/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/vitest-pool-workers/test/validation.test.ts b/packages/vitest-pool-workers/test/validation.test.ts index f43fdea5e7c..3d3e1c19655 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 }, } `; @@ -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 }, 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/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/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/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/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 9997e287cb1..4e1c4bd18ef 100644 --- a/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts +++ b/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts @@ -194,14 +194,11 @@ 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, 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, @@ -408,7 +405,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 diff --git a/packages/wrangler/src/api/startDevWorker/ProxyController.ts b/packages/wrangler/src/api/startDevWorker/ProxyController.ts index 991c6b1657f..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, }, ], @@ -133,7 +133,6 @@ export class ProxyController extends Controller { this.localServerReady.promise ), handleStructuredLogs, - liveReload: false, }; if (this.inspectorEnabled) { @@ -173,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/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/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 ff4be0e2aca..d2c8964f6b2 100644 --- a/packages/wrangler/src/dev/miniflare/index.ts +++ b/packages/wrangler/src/dev/miniflare/index.ts @@ -87,13 +87,10 @@ 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"]; localProtocol: "http" | "https"; - httpsKeyPath: string | undefined; - httpsCertPath: string | undefined; localUpstream: string | undefined; upstreamProtocol: "http" | "https"; inspect: boolean; @@ -466,7 +463,6 @@ type WorkerOptionsBindings = Pick< | "serviceBindings" | "ratelimits" | "workflows" - | "wrappedBindings" | "secretsStoreSecrets" | "images" | "email" @@ -696,8 +692,6 @@ export function buildMiniflareBindingOptions( const externalWorkers: WorkerOptions[] = []; - const wrappedBindings: WorkerOptions["wrappedBindings"] = {}; - for (const ai of aiBindings) { warnOrError("ai", ai.remote); } @@ -1085,7 +1079,6 @@ export function buildMiniflareBindingOptions( }) ), serviceBindings, - wrappedBindings: wrappedBindings, tails, streamingTails, }; @@ -1159,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 = { @@ -1169,7 +1164,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, @@ -1188,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), @@ -1202,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/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/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, 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; } 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 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}".` );