diff --git a/packages/miniflare/src/index.ts b/packages/miniflare/src/index.ts index 765bd02ab..b764a8d60 100644 --- a/packages/miniflare/src/index.ts +++ b/packages/miniflare/src/index.ts @@ -9,8 +9,12 @@ import { Duplex, Transform, Writable } from "stream"; import { ReadableStream } from "stream/web"; import zlib from "zlib"; import type { + CacheStorage, D1Database, + DurableObjectNamespace, + KVNamespace, Queue, + R2Bucket, RequestInitCfProperties, } from "@cloudflare/workers-types/experimental"; import exitHook from "exit-hook"; @@ -30,16 +34,13 @@ import { registerAllowUnauthorizedDispatcher, } from "./http"; import { - CacheStorage, D1_PLUGIN_NAME, DURABLE_OBJECTS_PLUGIN_NAME, DispatchFetch, DurableObjectClassNames, - DurableObjectNamespace, GatewayConstructor, GatewayFactory, HEADER_CF_BLOB, - KVNamespace, KV_PLUGIN_NAME, PLUGIN_ENTRIES, Persistence, @@ -49,8 +50,8 @@ import { QUEUES_PLUGIN_NAME, QueueConsumers, QueuesError, - R2Bucket, R2_PLUGIN_NAME, + ReplaceWorkersTypes, SharedOptions, SourceMapRegistry, WorkerOptions, @@ -1207,9 +1208,10 @@ export class Miniflare { return proxy as T; } // TODO(someday): would be nice to define these in plugins - async getCaches(): Promise { + async getCaches(): Promise> { const proxyClient = await this._getProxyClient(); - return proxyClient.global.caches as unknown as CacheStorage; + return proxyClient.global + .caches as unknown as ReplaceWorkersTypes; } getD1Database(bindingName: string, workerName?: string): Promise { return this.#getProxy(D1_PLUGIN_NAME, bindingName, workerName); @@ -1217,13 +1219,13 @@ export class Miniflare { getDurableObjectNamespace( bindingName: string, workerName?: string - ): Promise { + ): Promise> { return this.#getProxy(DURABLE_OBJECTS_PLUGIN_NAME, bindingName, workerName); } getKVNamespace( bindingName: string, workerName?: string - ): Promise { + ): Promise> { return this.#getProxy(KV_PLUGIN_NAME, bindingName, workerName); } getQueueProducer( @@ -1232,7 +1234,10 @@ export class Miniflare { ): Promise> { return this.#getProxy(QUEUES_PLUGIN_NAME, bindingName, workerName); } - getR2Bucket(bindingName: string, workerName?: string): Promise { + getR2Bucket( + bindingName: string, + workerName?: string + ): Promise> { return this.#getProxy(R2_PLUGIN_NAME, bindingName, workerName); } diff --git a/packages/miniflare/src/plugins/core/proxy/types.ts b/packages/miniflare/src/plugins/core/proxy/types.ts index e91da5cc0..bd8c6f51f 100644 --- a/packages/miniflare/src/plugins/core/proxy/types.ts +++ b/packages/miniflare/src/plugins/core/proxy/types.ts @@ -2,34 +2,17 @@ import { Blob } from "buffer"; import { arrayBuffer } from "stream/consumers"; import { ReadableStream } from "stream/web"; import type { + AbortSignal as WorkerAbortSignal, Blob as WorkerBlob, - CacheQueryOptions as WorkerCacheQueryOptions, - DurableObjectId as WorkerDurableObjectId, - DurableObjectJurisdiction as WorkerDurableObjectJurisdiction, - DurableObjectNamespace as WorkerDurableObjectNamespace, - DurableObjectNamespaceGetDurableObjectOptions as WorkerDurableObjectNamespaceGetDurableObjectOptions, - DurableObjectStub as WorkerDurableObjectStub, File as WorkerFile, Headers as WorkerHeaders, - KVNamespace as WorkerKVNamespace, - KVNamespaceGetOptions as WorkerKVNamespaceGetOptions, - KVNamespaceGetWithMetadataResult as WorkerKVNamespaceGetWithMetadataResult, - R2Bucket as WorkerR2Bucket, - R2Conditional as WorkerR2Conditional, - R2HTTPMetadata as WorkerR2HTTPMetadata, - R2ListOptions as WorkerR2ListOptions, - R2MultipartOptions as WorkerR2MultipartOptions, - R2MultipartUpload as WorkerR2MultipartUpload, - R2Object as WorkerR2Object, - R2ObjectBody as WorkerR2ObjectBody, - R2PutOptions as WorkerR2PutOptions, - R2Range as WorkerR2Range, - R2UploadedPart as WorkerR2UploadedPart, + ReadableStream as WorkerReadableStream, Request as WorkerRequest, + RequestInit as WorkerRequestInit, Response as WorkerResponse, } from "@cloudflare/workers-types/experimental"; import { File, Headers } from "undici"; -import { Request, RequestInfo, RequestInit, Response } from "../../../http"; +import { Request, RequestInit, Response } from "../../../http"; import { PlatformImpl } from "../../../workers"; export const NODE_PLATFORM_IMPL: PlatformImpl = { @@ -52,205 +35,157 @@ export const NODE_PLATFORM_IMPL: PlatformImpl = { }, }; -// Replacing `Request`, `Response` -export type Cache = { - delete( - request: RequestInfo, - options?: WorkerCacheQueryOptions - ): Promise; - match( - request: RequestInfo, - options?: WorkerCacheQueryOptions - ): Promise; - put(request: RequestInfo, response: Response): Promise; -}; -export type CacheStorage = { - open(cacheName: string): Promise; - readonly default: Cache; -}; +// Substitutes workers types with the corresponding Node implementations. +// prettier-ignore +export type ReplaceWorkersTypes = + T extends WorkerRequest ? Request : + T extends WorkerResponse ? Response : + T extends WorkerReadableStream ? ReadableStream : + Required extends Required ? RequestInit : + T extends WorkerHeaders ? Headers : + T extends WorkerBlob ? Blob : + T extends WorkerAbortSignal ? AbortSignal : + T extends Promise ? Promise> : + T extends (...args: infer P) => infer R ? (...args: ReplaceWorkersTypes

) => ReplaceWorkersTypes : + T extends object ? { [K in keyof T]: OverloadReplaceWorkersTypes } : + T; -// Replacing `Request`, `Response` -export type Fetcher = { - fetch(input: RequestInfo, init?: RequestInit): Promise; -}; +export type OverloadReplaceWorkersTypes = T extends (...args: any[]) => any + ? UnionToIntersection>> + : ReplaceWorkersTypes; -// Replacing `Request`, `Response` -export type DurableObjectStub = Omit & - Fetcher; -export type DurableObjectNamespace = Omit< - WorkerDurableObjectNamespace, - "get" | "getExisting" | "jurisdiction" -> & { - get( - id: WorkerDurableObjectId, - options?: WorkerDurableObjectNamespaceGetDurableObjectOptions - ): DurableObjectStub; - getExisting( - id: WorkerDurableObjectId, - options?: WorkerDurableObjectNamespaceGetDurableObjectOptions - ): DurableObjectStub; - jurisdiction( - jurisdiction: WorkerDurableObjectJurisdiction - ): DurableObjectNamespace; -}; +export type UnionToIntersection = ( + U extends any ? (k: U) => void : never +) extends (k: infer I) => void + ? I + : never; -// Replacing `ReadableStream` -export type KVNamespace = Omit & { - get( - key: string, - options?: Partial> - ): Promise; - get(key: string, type: "text"): Promise; - get( - key: string, - type: "json" - ): Promise; - get(key: string, type: "arrayBuffer"): Promise; - get(key: string, type: "stream"): Promise; - get( - key: string, - options?: WorkerKVNamespaceGetOptions<"text"> - ): Promise; - get( - key: string, - options?: WorkerKVNamespaceGetOptions<"json"> - ): Promise; - get( - key: string, - options?: WorkerKVNamespaceGetOptions<"arrayBuffer"> - ): Promise; - get( - key: string, - options?: WorkerKVNamespaceGetOptions<"stream"> - ): Promise; +export type OverloadUnion2 = T extends { + (...args: infer P1): infer R1; + (...args: infer P2): infer R2; +} + ? ((...args: P1) => R1) | ((...args: P2) => R2) + : T; - getWithMetadata( - key: string, - options?: Partial> - ): Promise>; - getWithMetadata( - key: string, - type: "text" - ): Promise>; - getWithMetadata( - key: string, - type: "json" - ): Promise>; - getWithMetadata( - key: string, - type: "arrayBuffer" - ): Promise>; - getWithMetadata( - key: string, - type: "stream" - ): Promise>; - getWithMetadata( - key: string, - options: WorkerKVNamespaceGetOptions<"text"> - ): Promise>; - getWithMetadata( - key: string, - options: WorkerKVNamespaceGetOptions<"json"> - ): Promise>; - getWithMetadata( - key: string, - options: WorkerKVNamespaceGetOptions<"arrayBuffer"> - ): Promise>; - getWithMetadata( - key: string, - options: WorkerKVNamespaceGetOptions<"stream"> - ): Promise>; -}; +export type OverloadUnion3 = T extends { + (...args: infer P1): infer R1; + (...args: infer P2): infer R2; + (...args: infer P3): infer R3; +} + ? ((...args: P1) => R1) | ((...args: P2) => R2) | ((...args: P3) => R3) + : OverloadUnion2; -// Replacing `Headers`, `ReadableStream`, `Blob` -export type R2Object = Omit & { - writeHttpMetadata(headers: Headers): void; -}; -export type R2ObjectBody = Omit< - WorkerR2ObjectBody, - "writeHttpMetadata" | "body" | "blob" -> & { - writeHttpMetadata(headers: Headers): void; - get body(): ReadableStream; - blob(): Promise; -}; -export type R2GetOptions = { - onlyIf?: WorkerR2Conditional | Headers; - range?: WorkerR2Range | Headers; -}; -export type R2PutOptions = Omit< - WorkerR2PutOptions, - "onlyIf" | "httpMetadata" -> & { - onlyIf?: WorkerR2Conditional | Headers; - httpMetadata?: WorkerR2HTTPMetadata | Headers; -}; -export type R2MultipartOptions = Omit< - WorkerR2MultipartOptions, - "httpMetadata" -> & { - httpMetadata?: WorkerR2HTTPMetadata | Headers; -}; -export type R2MultipartUpload = Omit< - WorkerR2MultipartUpload, - "uploadPart" | "complete" -> & { - uploadPart( - partNumber: number, - value: ReadableStream | (ArrayBuffer | ArrayBufferView) | string | Blob - ): Promise; - complete(uploadedParts: WorkerR2UploadedPart[]): Promise; -}; -export type R2Objects = { - objects: R2Object[]; - delimitedPrefixes: string[]; -} & ({ truncated: true; cursor: string } | { truncated: false }); +export type OverloadUnion4 = T extends { + (...args: infer P1): infer R1; + (...args: infer P2): infer R2; + (...args: infer P3): infer R3; + (...args: infer P4): infer R4; +} + ? + | ((...args: P1) => R1) + | ((...args: P2) => R2) + | ((...args: P3) => R3) + | ((...args: P4) => R4) + : OverloadUnion3; -export type R2Bucket = Omit< - WorkerR2Bucket, - | "head" - | "get" - | "put" - | "createMultipartUpload" - | "resumeMultipartUpload" - | "list" -> & { - head(key: string): Promise; - get( - key: string, - options: R2GetOptions & { - onlyIf: WorkerR2Conditional | Headers; - } - ): Promise; - get(key: string, options?: R2GetOptions): Promise; - put( - key: string, - value: - | ReadableStream - | ArrayBuffer - | ArrayBufferView - | string - | null - | Blob, - options?: R2PutOptions - ): Promise; - put( - key: string, - value: - | ReadableStream - | ArrayBuffer - | ArrayBufferView - | string - | null - | Blob, - options?: R2PutOptions & { - onlyIf: WorkerR2Conditional | Headers; - } - ): Promise; - createMultipartUpload( - key: string, - options?: R2MultipartOptions - ): Promise; - resumeMultipartUpload(key: string, uploadId: string): R2MultipartUpload; - list(options?: WorkerR2ListOptions): Promise; -}; +export type OverloadUnion5 = T extends { + (...args: infer P1): infer R1; + (...args: infer P2): infer R2; + (...args: infer P3): infer R3; + (...args: infer P4): infer R4; + (...args: infer P5): infer R5; +} + ? + | ((...args: P1) => R1) + | ((...args: P2) => R2) + | ((...args: P3) => R3) + | ((...args: P4) => R4) + | ((...args: P5) => R5) + : OverloadUnion4; + +export type OverloadUnion6 = T extends { + (...args: infer P1): infer R1; + (...args: infer P2): infer R2; + (...args: infer P3): infer R3; + (...args: infer P4): infer R4; + (...args: infer P5): infer R5; + (...args: infer P6): infer R6; +} + ? + | ((...args: P1) => R1) + | ((...args: P2) => R2) + | ((...args: P3) => R3) + | ((...args: P4) => R4) + | ((...args: P5) => R5) + | ((...args: P6) => R6) + : OverloadUnion5; + +export type OverloadUnion7 = T extends { + (...args: infer P1): infer R1; + (...args: infer P2): infer R2; + (...args: infer P3): infer R3; + (...args: infer P4): infer R4; + (...args: infer P5): infer R5; + (...args: infer P6): infer R6; + (...args: infer P7): infer R7; +} + ? + | ((...args: P1) => R1) + | ((...args: P2) => R2) + | ((...args: P3) => R3) + | ((...args: P4) => R4) + | ((...args: P5) => R5) + | ((...args: P6) => R6) + | ((...args: P7) => R7) + : OverloadUnion6; + +export type OverloadUnion8 = T extends { + (...args: infer P1): infer R1; + (...args: infer P2): infer R2; + (...args: infer P3): infer R3; + (...args: infer P4): infer R4; + (...args: infer P5): infer R5; + (...args: infer P6): infer R6; + (...args: infer P7): infer R7; + (...args: infer P8): infer R8; +} + ? + | ((...args: P1) => R1) + | ((...args: P2) => R2) + | ((...args: P3) => R3) + | ((...args: P4) => R4) + | ((...args: P5) => R5) + | ((...args: P6) => R6) + | ((...args: P7) => R7) + | ((...args: P8) => R8) + : OverloadUnion7; + +// `KVNamespace#{get,getWithMetadata}()` each have 9 overloads :D +export type OverloadUnion9 = T extends { + (...args: infer P1): infer R1; + (...args: infer P2): infer R2; + (...args: infer P3): infer R3; + (...args: infer P4): infer R4; + (...args: infer P5): infer R5; + (...args: infer P6): infer R6; + (...args: infer P7): infer R7; + (...args: infer P8): infer R8; + (...args: infer P9): infer R9; +} + ? + | ((...args: P1) => R1) + | ((...args: P2) => R2) + | ((...args: P3) => R3) + | ((...args: P4) => R4) + | ((...args: P5) => R5) + | ((...args: P6) => R6) + | ((...args: P7) => R7) + | ((...args: P8) => R8) + | ((...args: P9) => R9) + : OverloadUnion8; + +export type OverloadUnion any> = + // Functions with no parameters pass the `extends` checks in the + // `OverloadUnionN` types with `(...args: unknown[]) => unknown` for the + // other overloads. Therefore, filter them out early. + Parameters extends [] ? T : OverloadUnion9; diff --git a/packages/miniflare/test/plugins/cache/index.spec.ts b/packages/miniflare/test/plugins/cache/index.spec.ts index 85cc66bcb..83fc888d4 100644 --- a/packages/miniflare/test/plugins/cache/index.spec.ts +++ b/packages/miniflare/test/plugins/cache/index.spec.ts @@ -2,11 +2,12 @@ import assert from "assert"; import crypto from "crypto"; import path from "path"; import { text } from "stream/consumers"; +import type { CacheStorage } from "@cloudflare/workers-types/experimental"; import { - CacheStorage, HeadersInit, KeyValueStorage, LogLevel, + ReplaceWorkersTypes, Request, RequestInit, Response, @@ -15,7 +16,7 @@ import { import { MiniflareTestContext, miniflareTest, useTmp } from "../../test-shared"; interface Context extends MiniflareTestContext { - caches: CacheStorage; + caches: ReplaceWorkersTypes; } const test = miniflareTest({}, async (global, req) => { diff --git a/packages/miniflare/test/plugins/core/proxy/client.spec.ts b/packages/miniflare/test/plugins/core/proxy/client.spec.ts index b544ff82f..277ab0117 100644 --- a/packages/miniflare/test/plugins/core/proxy/client.spec.ts +++ b/packages/miniflare/test/plugins/core/proxy/client.spec.ts @@ -3,13 +3,14 @@ import { Blob } from "buffer"; import { text } from "stream/consumers"; import { ReadableStream } from "stream/web"; import util from "util"; +import type { Fetcher } from "@cloudflare/workers-types/experimental"; import test, { ThrowsExpectation } from "ava"; import { DeferredPromise, - Fetcher, File, MessageEvent, Miniflare, + ReplaceWorkersTypes, Response, WebSocketPair, } from "miniflare"; @@ -34,7 +35,9 @@ test("ProxyClient: supports service bindings with WebSockets", async (t) => { }, }, }); - const { CUSTOM } = await mf.getBindings<{ CUSTOM: Fetcher }>(); + const { CUSTOM } = await mf.getBindings<{ + CUSTOM: ReplaceWorkersTypes; + }>(); const res = await CUSTOM.fetch("http://placeholder/", { headers: { Upgrade: "websocket" }, diff --git a/packages/miniflare/test/plugins/r2/index.spec.ts b/packages/miniflare/test/plugins/r2/index.spec.ts index 771299e53..812aef9e4 100644 --- a/packages/miniflare/test/plugins/r2/index.spec.ts +++ b/packages/miniflare/test/plugins/r2/index.spec.ts @@ -5,9 +5,11 @@ import crypto from "crypto"; import path from "path"; import { text } from "stream/consumers"; import type { + R2Bucket, R2Conditional, R2ListOptions, - R2Bucket as WorkersR2Bucket, + R2Object, + R2Objects, } from "@cloudflare/workers-types/experimental"; import { Macro, ThrowsExpectation } from "ava"; import { @@ -15,11 +17,8 @@ import { MiniflareOptions, MultipartPartRow, ObjectRow, - R2Bucket, R2Gateway, - R2Object, - R2ObjectBody, - R2Objects, + ReplaceWorkersTypes, Storage, TypedDatabase, createFileStorage, @@ -48,10 +47,13 @@ function hash(value: string, algorithm = "md5") { return crypto.createHash(algorithm).update(value).digest("hex"); } -type NamespacedR2Bucket = R2Bucket & { ns: string }; +type NamespacedR2Bucket = ReplaceWorkersTypes & { ns: string }; // Automatically prefix all keys with the specified namespace -function nsBucket(ns: string, bucket: R2Bucket): NamespacedR2Bucket { +function nsBucket( + ns: string, + bucket: ReplaceWorkersTypes +): NamespacedR2Bucket { return new Proxy(bucket as NamespacedR2Bucket, { get(target, key, receiver) { if (key === "ns") return ns; @@ -86,7 +88,7 @@ const opts: Partial = { r2Buckets: { BUCKET: "bucket" }, compatibilityFlags: ["r2_list_honor_include"], }; -const test = miniflareTest<{ BUCKET: WorkersR2Bucket }, Context>( +const test = miniflareTest<{ BUCKET: R2Bucket }, Context>( opts, async (global) => { return new global.Response(null, { status: 404 }); @@ -167,6 +169,7 @@ test("head: returns metadata for existing keys", async (t) => { // Test proxying of `writeHttpMetadata()` const headers = new Headers({ "X-Key": "value" }); + // noinspection JSVoidFunctionReturnValueUsed t.is(object.writeHttpMetadata(headers), undefined); t.is(headers.get("Content-Type"), "text/plain"); t.is(headers.get("X-Key"), "value"); @@ -215,6 +218,7 @@ test("get: returns metadata and body for existing keys", async (t) => { // Test proxying of `writeHttpMetadata()` const headers = new Headers({ "X-Key": "value" }); + // noinspection JSVoidFunctionReturnValueUsed t.is(body.writeHttpMetadata(headers), undefined); t.is(headers.get("Content-Type"), "text/plain"); t.is(headers.get("X-Key"), "value"); @@ -490,7 +494,7 @@ test("put: stores only if passes onlyIf", async (t) => { }; const fail = async (cond: R2Conditional) => { const object = await r2.put("key", "2", { onlyIf: cond }); - t.is(object as R2Object | null, null); + t.is(object as ReplaceWorkersTypes | null, null); t.is(await (await r2.get("key"))?.text(), "1"); // No `reset()` as we've just checked we didn't update anything }; @@ -841,9 +845,9 @@ test("list: returns correct delimitedPrefixes for delimiter and prefix", async ( const allKeys = Object.keys(values); for (const [key, value] of Object.entries(values)) await r2.put(key, value); - const keys = (result: R2Objects) => + const keys = (result: ReplaceWorkersTypes) => result.objects.map(({ key }) => key.substring(ns.length)); - const delimitedPrefixes = (result: R2Objects) => + const delimitedPrefixes = (result: ReplaceWorkersTypes) => result.delimitedPrefixes.map((prefix) => prefix.substring(ns.length)); const allKeysWithout = (...exclude: string[]) => allKeys.filter((value) => !exclude.includes(value)); @@ -1391,18 +1395,18 @@ test("get: is multipart aware", async (t) => { // Check ranged get accessing single part const halfPartSize = Math.floor(PART_SIZE / 2); const quarterPartSize = Math.floor(PART_SIZE / 4); - object = (await r2.get("key", { + object = await r2.get("key", { range: { offset: halfPartSize, length: quarterPartSize }, - })) as R2ObjectBody | null; + }); t.is(await object?.text(), "a".repeat(quarterPartSize)); // Check ranged get accessing multiple parts - object = (await r2.get("key", { + object = await r2.get("key", { range: { offset: halfPartSize, length: halfPartSize + PART_SIZE + quarterPartSize, }, - })) as R2ObjectBody | null; + }); t.is( await object?.text(), `${"a".repeat(halfPartSize)}${"b".repeat(PART_SIZE)}${"c".repeat( @@ -1411,9 +1415,9 @@ test("get: is multipart aware", async (t) => { ); // Check ranged get of suffix - object = (await r2.get("key", { + object = await r2.get("key", { range: { suffix: quarterPartSize + PART_SIZE }, - })) as R2ObjectBody | null; + }); t.is( await object?.text(), `${"b".repeat(quarterPartSize)}${"c".repeat(PART_SIZE)}`