Last active
July 15, 2025 14:00
-
-
Save xesrevinu/cf545dda28df77fdaf5b049aa44c0bc6 to your computer and use it in GitHub Desktop.
Simple Cloudflare Miniflare effect layer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { D1Database, DurableObjectNamespace, KVNamespace, Queue, R2Bucket, Socket, SocketAddress, SocketOptions } from '@cloudflare/workers-types' | |
| import { FileSystem, Path } from '@effect/platform' | |
| import type { PlatformError } from '@effect/platform/Error' | |
| import * as Context from 'effect/Context' | |
| import * as Effect from 'effect/Effect' | |
| import * as Layer from 'effect/Layer' | |
| import * as LogLevelE from 'effect/LogLevel' | |
| import * as Esbuild from 'esbuild' | |
| import { Log, LogLevel, Miniflare as MiniflareBase, type MiniflareOptions, type WorkerOptions } from 'miniflare' | |
| import { unstable_readConfig } from 'wrangler' | |
| interface TestModule { | |
| path: string | |
| config: string | |
| } | |
| interface TestOptions { | |
| cwd: string | |
| port?: number | undefined | |
| alias?: Record<string, string> | undefined | |
| } | |
| export const make = (options: TestOptions, modules: TestModule[]) => | |
| Effect.gen(function* () { | |
| const { cwd, alias, port = 5999 } = options | |
| const fs = yield* FileSystem.FileSystem | |
| const { join, basename } = yield* Path.Path | |
| const scriptOutputPath = join(cwd, '.scripts') | |
| const miniflarCachePath = '.miniflare-cache' | |
| const cachePersistPath = join(miniflarCachePath, 'cache') | |
| const d1PersistPath = join(miniflarCachePath, 'd1') | |
| const kvPersistPath = join(miniflarCachePath, 'kv') | |
| const r2PersistPath = join(miniflarCachePath, 'r2') | |
| const durableObjectsPersistPath = join(miniflarCachePath, 'durable-objects') | |
| const workflowsPersistPath = join(miniflarCachePath, 'workflows') | |
| const analyticsEngineDatasetsPersistPath = join(miniflarCachePath, 'analytics-engine-datasets') | |
| const cachePersist = join(cwd, cachePersistPath) | |
| const d1Persist = join(cwd, d1PersistPath) | |
| const kvPersist = join(cwd, kvPersistPath) | |
| const r2Persist = join(cwd, r2PersistPath) | |
| const durableObjectsPersist = join(cwd, durableObjectsPersistPath) | |
| const workflowsPersist = join(cwd, workflowsPersistPath) | |
| const analyticsEngineDatasetsPersist = join(cwd, analyticsEngineDatasetsPersistPath) | |
| const wranglerConfigs = modules.map((_) => unstable_readConfig({ config: _.config })) | |
| const scripts = yield* Effect.forEach( | |
| wranglerConfigs, | |
| (item) => | |
| Effect.promise(() => | |
| Esbuild.build({ | |
| entryPoints: [item.main!], | |
| bundle: true, | |
| write: false, | |
| format: 'esm', | |
| target: 'es2022', | |
| platform: 'browser', | |
| define: { | |
| 'process.env.NODE_ENV': '"development"', | |
| }, | |
| legalComments: 'none', | |
| treeShaking: true, | |
| metafile: true, | |
| sourcemap: 'linked', | |
| outfile: `${item.name!}.js`, | |
| external: ['cloudflare:*', 'node:*'], | |
| alias, | |
| }), | |
| ), | |
| { | |
| concurrency: 3, | |
| }, | |
| ).pipe( | |
| Effect.map((results) => | |
| results.flatMap((result) => { | |
| const outputName = basename(result.outputFiles[0].path).replace('.map', '').replace('.js', '') | |
| const metafileJson = JSON.stringify(result.metafile, null, 2) | |
| const metafile = { | |
| main: false, | |
| filename: `${outputName}.metafile.json`, | |
| contents: new TextEncoder().encode(metafileJson), | |
| text: metafileJson, | |
| } | |
| return result.outputFiles | |
| .map((file) => { | |
| const filename = basename(file.path) | |
| const isMain = filename.indexOf('.map') === -1 | |
| return { | |
| filename, | |
| main: isMain, | |
| contents: file.contents, | |
| text: file.text, | |
| } | |
| }) | |
| .concat(metafile) | |
| }), | |
| ), | |
| ) | |
| yield* Effect.forEach(scripts, (script) => { | |
| const path = join(scriptOutputPath, script.filename) | |
| return fs.writeFile(path, script.contents) | |
| }) | |
| const workerScripts = scripts.filter((script) => !!script.main) | |
| const workers: WorkerOptions[] = wranglerConfigs.map((config, index) => { | |
| const script = workerScripts[index].text | |
| return { | |
| name: config.name, | |
| modules: true, | |
| script, | |
| compatibilityFlags: config.compatibility_flags, | |
| compatibilityDate: config.compatibility_date, | |
| cache: true, | |
| d1Databases: Object.fromEntries( | |
| config.d1_databases.map((_) => { | |
| return [_.binding, _.database_id || ''] | |
| }), | |
| ), | |
| kvNamespaces: Object.fromEntries( | |
| config.kv_namespaces.map((_) => { | |
| return [_.binding, _.id || ''] | |
| }), | |
| ), | |
| r2Buckets: Object.fromEntries( | |
| config.r2_buckets.map((_) => { | |
| return [_.binding, _.bucket_name || ''] | |
| }), | |
| ), | |
| durableObjects: Object.fromEntries( | |
| config.durable_objects.bindings.map((_) => { | |
| return [ | |
| _.name, | |
| { | |
| className: _.class_name, | |
| scriptName: _.script_name, | |
| useSQLite: true, | |
| }, | |
| ] | |
| }), | |
| ), | |
| serviceBindings: Object.fromEntries( | |
| (config.services || []).map((_) => { | |
| return [ | |
| _.binding, | |
| { | |
| name: _.service, | |
| entrypoint: _.entrypoint, | |
| }, | |
| ] | |
| }), | |
| ), | |
| workflows: Object.fromEntries( | |
| config.workflows.map((_) => { | |
| return [ | |
| _.name, | |
| { | |
| name: _.name, | |
| className: _.class_name, | |
| scriptName: _.script_name, | |
| }, | |
| ] | |
| }), | |
| ), | |
| analyticsEngineDatasets: Object.fromEntries( | |
| config.analytics_engine_datasets.map((_) => { | |
| return [ | |
| _.binding, | |
| { | |
| dataset: _.dataset || '', | |
| }, | |
| ] | |
| }), | |
| ), | |
| bindings: { | |
| NODE_ENV: 'development', | |
| LOG_LEVEL: LogLevelE.All._tag, | |
| STAGE: 'test', | |
| }, | |
| } satisfies WorkerOptions | |
| }) | |
| const miniflare = new MiniflareBase({ | |
| log: new Log(LogLevel.DEBUG), // Logger Miniflare uses for debugging | |
| port, | |
| inspectorPort: port - 1, | |
| cachePersist, | |
| d1Persist, | |
| kvPersist, | |
| r2Persist, | |
| durableObjectsPersist, | |
| workflowsPersist, | |
| analyticsEngineDatasetsPersist, | |
| workers, | |
| liveReload: false, | |
| verbose: false, | |
| }) | |
| yield* Effect.addFinalizer(() => Effect.promise(() => miniflare.dispose())) | |
| yield* Effect.promise(() => miniflare.ready) | |
| return { | |
| getCf: () => Effect.promise(() => miniflare.getCf()), | |
| getInspectorURL: () => Effect.promise(() => miniflare.getInspectorURL()), | |
| setOptions: (opts: MiniflareOptions) => Effect.promise(() => miniflare.setOptions(opts)), | |
| url: Effect.sync(() => new URL(`http://localhost:${port}`)), | |
| fetch: (input: globalThis.RequestInfo | URL, init?: globalThis.RequestInit) => | |
| Effect.promise(() => miniflare.dispatchFetch(input as any, init as any) as unknown as Promise<Response>), | |
| unsafeGetDirectURL: (workerName: string) => Effect.promise(() => miniflare.unsafeGetDirectURL(workerName)), | |
| getBindings: <Env = Record<string, unknown>>(workerName?: string) => | |
| Effect.promise(() => miniflare.getBindings<Env>(workerName)), | |
| getWorker: (workerName?: string | undefined) => Effect.promise(() => miniflare.getWorker(workerName)), | |
| getCaches: () => Effect.promise(() => miniflare.getCaches()), | |
| getD1Database: (bindingName: string, workerName?: string) => | |
| Effect.promise(() => miniflare.getD1Database(bindingName, workerName)), | |
| getDurableObjectNamespace: (bindingName: string, workerName?: string) => | |
| Effect.promise(() => miniflare.getDurableObjectNamespace(bindingName, workerName)), | |
| getKVNamespace: (bindingName: string, workerName?: string) => | |
| Effect.promise(() => miniflare.getKVNamespace(bindingName, workerName)), | |
| getQueueProducer: <Body = unknown>(bindingName: string, workerName?: string) => | |
| Effect.promise(() => miniflare.getQueueProducer<Body>(bindingName, workerName)), | |
| getR2Bucket: (bindingName: string, workerName?: string) => | |
| Effect.promise(() => miniflare.getR2Bucket(bindingName, workerName)), | |
| } | |
| }) | |
| type Fetcher = { | |
| fetch(input: globalThis.RequestInfo | URL, init?: globalThis.RequestInit): Promise<globalThis.Response> | |
| connect(address: SocketAddress | string, options?: SocketOptions): Socket | |
| } | |
| export class Miniflare extends Context.Tag('Miniflare')< | |
| Miniflare, | |
| { | |
| getCf(): Effect.Effect<Record<string, any>> | |
| getInspectorURL(): Effect.Effect<URL> | |
| url: Effect.Effect<URL> | |
| setOptions(opts: MiniflareOptions): Effect.Effect<void> | |
| fetch: (input: globalThis.RequestInfo | URL, init?: globalThis.RequestInit) => Effect.Effect<globalThis.Response> | |
| unsafeGetDirectURL: (workerName: string) => Effect.Effect<URL, never, never> | |
| getBindings<Env = Record<string, unknown>>(workerName?: string | undefined): Effect.Effect<Env> | |
| getWorker(workerName?: string | undefined): Effect.Effect<{ | |
| fetch: Fetcher['fetch'] | |
| }> | |
| getCaches(): Effect.Effect<CacheStorage> | |
| getD1Database(bindingName: string, workerName?: string | undefined): Effect.Effect<D1Database> | |
| getDurableObjectNamespace( | |
| bindingName: string, | |
| workerName?: string | undefined, | |
| ): Effect.Effect<DurableObjectNamespace> | |
| getKVNamespace(bindingName: string, workerName?: string | undefined): Effect.Effect<KVNamespace> | |
| getQueueProducer<Body = unknown>(bindingName: string, workerName?: string | undefined): Effect.Effect<Queue<Body>> | |
| getR2Bucket(bindingName: string, workerName?: string | undefined): Effect.Effect<R2Bucket> | |
| } | |
| >() { | |
| static Config: ( | |
| options: TestOptions, | |
| modules: TestModule[], | |
| ) => Layer.Layer<Miniflare, PlatformError, FileSystem.FileSystem | Path.Path> = ( | |
| options: TestOptions, | |
| modules: TestModule[], | |
| // F**K CF Types | |
| ) => Layer.scoped(Miniflare, make(options, modules) as any) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment