import assert from "assert" ; import fs from "fs/promises" ; import path from "path" ; import { Awaitable , Context , Mount , Option , OptionType , Plugin , PluginContext , RequestContext , SetupResult , getRequestContext , } from "@miniflare/shared" ; import dotenv from "dotenv" ; import { MiniflareCoreError } from "../error" ; import { Request , RequestInfo , RequestInit , Response } from "../standards" ; const kWranglerBindings = Symbol ( "kWranglerBindings" ) ; internal export type _CoreMount = Mount < Request , Response > ; export type FetcherFetch = ( request : Request ) => Awaitable < Response > ; export type ServiceBindingsOptions = Record < string , string | { service : string ; environment ? : string ; } | FetcherFetch > ; interface ProcessedServiceBinding { name : string ; service : string | FetcherFetch ; environment : string ; } export interface BindingsOptions { envPath ? : boolean | string ; envPathDefaultFallback ? : boolean ; bindings ? : Record < string , any > ; globals ? : Record < string , any > ; wasmBindings ? : Record < string , string > ; serviceBindings ? : ServiceBindingsOptions ; } export class Fetcher { readonly #service : string | FetcherFetch ; readonly #getServiceFetch : ( name : string ) => Promise < FetcherFetch > ; constructor ( service , getServiceFetch ) { this . #service = service ; this . #getServiceFetch = getServiceFetch ; } async fetch ( input , init ? ) { const parentCtx = getRequestContext ( ) ; const requestDepth = parentCtx ?. requestDepth ?? 1 ; const pipelineDepth = ( parentCtx ?. pipelineDepth ?? 0 ) + 1 ; const ctx = new RequestContext ( { requestDepth , pipelineDepth } ) ; const req = new Request ( input , init ) ; const fetch = typeof this . #service === "function" ? this . #service : await this . #getServiceFetch ( this . #service ) ; return ctx . runWith ( ( ) => fetch ( req ) ) ; } } export class BindingsPlugin extends Plugin < BindingsOptions > implements BindingsOptions { @ Option ( { type : OptionType . STRING , name : "env" , alias : "e" , description : "Path to .env file" , logValue ( value ) { if ( value === true ) return ".env" ; if ( value === false ) return undefined ; return path . relative ( "" , value ) ; } , fromWrangler : ( { miniflare } ) => miniflare ?. env_path , } ) envPath ? : boolean | string ; @ Option ( { type : OptionType . OBJECT , logName : "Wrangler Variables" , fromWrangler : ( { vars } ) => { if ( ! vars ) return ; return Object . fromEntries ( Object . entries ( vars ) . map ( ( [ key , value ] ) => [ key , String ( value ) ] ) ) ; } , } ) [ kWranglerBindings ] ? : Record < string , any > ; @ Option ( { type : OptionType . NONE } ) envPathDefaultFallback ? : boolean ; @ Option ( { type : OptionType . OBJECT , alias : "b" , description : "Binds variable/secret to environment" , logName : "Custom Bindings" , } ) bindings ? : Record < string , any > ; @ Option ( { type : OptionType . OBJECT , description : "Binds variable/secret to global scope" , logName : "Custom Globals" , fromWrangler : ( { miniflare } ) => miniflare ?. globals , } ) globals ? : Record < string , any > ; @ Option ( { type : OptionType . OBJECT , typeFormat : "NAME=PATH" , name : "wasm" , description : "WASM module to bind" , logName : "WASM Bindings" , fromWrangler : ( { wasm_modules } ) => wasm_modules , } ) wasmBindings ? : Record < string , string > ; @ Option ( { type : OptionType . OBJECT , typeFormat : "NAME=MOUNT[@ENV]" , name : "service" , alias : "S" , description : "Mounted service to bind" , fromEntries : ( entries ) => Object . fromEntries ( entries . map ( ( [ name , serviceEnvironment ] ) => { const atIndex = serviceEnvironment . indexOf ( "@" ) ; if ( atIndex === - 1 ) { return [ name , serviceEnvironment ] ; } else { const service = serviceEnvironment . substring ( 0 , atIndex ) ; const environment = serviceEnvironment . substring ( atIndex + 1 ) ; return [ name , { service , environment } ] ; } } ) ) , fromWrangler : ( { experimental_services } ) => experimental_services ?. reduce ( ( services , { name , service , environment } ) => { services [ name ] = { service , environment } ; return services ; } , { } as ServiceBindingsOptions ) , } ) serviceBindings ? : ServiceBindingsOptions ; readonly #processedServiceBindings : ProcessedServiceBinding [ ] ; #contextPromise ? : Promise < void > ; #contextResolve ? : ( ) => void ; #mounts ? : Map < string , _CoreMount > ; constructor ( ctx , options ? ) { super ( ctx ) ; this . assignOptions ( options ) ; if ( this . envPathDefaultFallback && this . envPath === undefined ) { this . envPath = true ; } this . #processedServiceBindings = Object . entries ( this . serviceBindings ?? { } ) . map ( ( [ name , options ] ) => { const service = typeof options === "object" ? options . service : options ; const environment = ( typeof options === "object" && options . environment ) || "production" ; return { name , service , environment } ; } ) ; if ( this . #processedServiceBindings . length ) { ctx . log . warn ( "Service bindings are experimental and primarily meant for internal " + "testing at the moment. There may be breaking changes in the future." ) ; } } #getServiceFetch = async ( service ) : Promise < FetcherFetch > => { assert ( this . #contextPromise , "beforeReload() must be called before #getServiceFetch()" ) ; await this . #contextPromise ; const fetch = this . #mounts ?. get ( service ) ?. dispatchFetch ; assert ( fetch ) ; return fetch ; } ; async setup ( ) { const bindings = { } ; const watch = [ ] ; Object . assign ( bindings , this [ kWranglerBindings ] ) ; let envPath = this . envPath === true ? ".env" : this . envPath ; if ( envPath ) { envPath = path . resolve ( this . ctx . rootPath , envPath ) ; try { Object . assign ( bindings , dotenv . parse ( await fs . readFile ( envPath , "utf8" ) ) ) ; } catch ( e ) { if ( ! ( e . code === "ENOENT" && this . envPath === true ) ) throw e ; } watch . push ( envPath ) ; } if ( this . wasmBindings ) { for ( let [ name , wasmPath ] of Object . entries ( this . wasmBindings ) ) { wasmPath = path . resolve ( this . ctx . rootPath , wasmPath ) ; bindings [ name ] = new WebAssembly . Module ( await fs . readFile ( wasmPath ) ) ; watch . push ( wasmPath ) ; } } for ( const { name , service } of this . #processedServiceBindings ) { bindings [ name ] = new Fetcher ( service , this . #getServiceFetch ) ; } Object . assign ( bindings , this . bindings ) ; return { globals : this . globals , bindings , watch } ; } beforeReload ( ) { this . #mounts = undefined ; this . #contextPromise = new Promise ( ( resolve ) => ( this . #contextResolve = resolve ) ) ; } reload ( bindings , moduleExports , mounts ) { for ( const { name , service } of this . #processedServiceBindings ) { if ( typeof service === "string" && ! mounts . has ( service ) ) { throw new MiniflareCoreError ( "ERR_SERVICE_NOT_MOUNTED" , `Service "${ service }" for binding "${ name }" not found.
Make sure "${ service }" is mounted so Miniflare knows where to find it.` ) ; } } this . #mounts = mounts ; assert ( this . #contextResolve , "beforeReload() must be called before reload()" ) ; this . #contextResolve ( ) ; } dispose ( ) { return this . beforeReload ( ) ; } }