import { Blob } from "buffer" ; import fs from "fs/promises" ; import path from "path" ; import { ByteLengthQueuingStrategy , CountQueuingStrategy , ReadableByteStreamController , ReadableStream , ReadableStreamBYOBReader , ReadableStreamBYOBRequest , ReadableStreamDefaultController , ReadableStreamDefaultReader , TransformStream , TransformStreamDefaultController , WritableStream , WritableStreamDefaultController , WritableStreamDefaultWriter , } from "stream/web" ; import { URL , URLSearchParams } from "url" ; import { TextEncoder } from "util" ; import { deserialize , serialize } from "v8" ; import { CompatibilityFlag , Context , ModuleRule , ModuleRuleType , Option , OptionType , Plugin , PluginContext , ProcessedModuleRule , STRING_SCRIPT_PATH , SetupResult , globsToMatcher , } from "@miniflare/shared" ; import { File , FormData , Headers } from "undici" ; import { MiniflareCoreError } from "../error" ; import { AbortSignal , DOMException , FetchEvent , FixedLengthStream , Request , Response , ScheduledEvent , Scheduler , TextDecoder , WorkerGlobalScope , atob , btoa , createCompatFetch , createCrypto , createTimer , withStringFormDataFiles , } from "../standards" ; import { assertsInRequest } from "../standards/helpers" ; import type { BindingsOptions } from "./bindings" ; const DEFAULT_MODULE_RULES = [ { type : "ESModule" , include : [ "**/*.mjs" ] } , { type : "CommonJS" , include : [ "**/*.js" , "**/*.cjs" ] } , ] ; function proxyStringFormDataFiles < Class extends typeof Request | typeof Response > ( klass ) { return new Proxy ( klass , { construct ( target , args , newTarget ) { const value = Reflect . construct ( target , args , newTarget ) ; return withStringFormDataFiles ( value ) ; } , } ) ; } function structuredCloneBuffer < T > ( value ) { return deserialize ( serialize ( value ) ) ; } export interface CoreOptions { script ? : string ; scriptPath ? : string ; rootPath ? : string ; packagePath ? : boolean | string ; wranglerConfigPath ? : boolean | string ; wranglerConfigEnv ? : string ; modules ? : boolean ; modulesRules ? : ModuleRule [ ] ; compatibilityDate ? : string ; compatibilityFlags ? : CompatibilityFlag [ ] ; upstream ? : string ; watch ? : boolean ; debug ? : boolean ; verbose ? : boolean ; updateCheck ? : boolean ; mounts ? : Record < string , string | CoreOptions | BindingsOptions > ; name ? : string ; routes ? : string [ ] ; logUnhandledRejections ? : boolean ; globalAsyncIO ? : boolean ; globalTimers ? : boolean ; globalRandom ? : boolean ; } function mapMountEntries ( [ name , pathEnv ] , relativeTo ? ) { let wranglerConfigEnv ; const atIndex = pathEnv . lastIndexOf ( "@" ) ; if ( atIndex !== - 1 ) { wranglerConfigEnv = pathEnv . substring ( atIndex + 1 ) ; pathEnv = pathEnv . substring ( 0 , atIndex ) ; } if ( relativeTo ) pathEnv = path . resolve ( relativeTo , pathEnv ) ; return [ name , { rootPath : pathEnv , wranglerConfigEnv , packagePath : true , envPath : true , wranglerConfigPath : true , } , ] ; } export class CorePlugin extends Plugin < CoreOptions > implements CoreOptions { @ Option ( { type : OptionType . NONE , logValue : ( ) => STRING_SCRIPT_PATH } ) script ? : string ; @ Option ( { type : OptionType . STRING_POSITIONAL , name : "script" , fromWrangler : ( { build } , configDir ) => build ?. upload ?. main ? path . resolve ( configDir , build ?. upload ?. dir ?? "dist" , build . upload . main ) : undefined , } ) scriptPath ? : string ; @ Option ( { type : OptionType . STRING , name : "wrangler-config" , alias : "c" , description : "Path to wrangler.toml" , logValue ( value ) { if ( value === true ) return "wrangler.toml" ; if ( value === false ) return undefined ; return path . relative ( "" , value ) ; } , } ) wranglerConfigPath ? : boolean | string ; @ Option ( { type : OptionType . STRING , name : "wrangler-env" , description : "Environment in wrangler.toml to use" , logName : "Wrangler Environment" , } ) wranglerConfigEnv ? : string ; @ Option ( { type : OptionType . STRING , name : "package" , description : "Path to package.json" , logValue ( value ) { if ( value === true ) return "package.json" ; if ( value === false ) return undefined ; return path . relative ( "" , value ) ; } , } ) packagePath ? : boolean | string ; @ Option ( { type : OptionType . BOOLEAN , alias : "m" , description : "Enable modules" , fromWrangler : ( { build } ) => build ?. upload ?. format && build . upload . format === "modules" , } ) modules ? : boolean ; @ Option ( { type : OptionType . OBJECT , typeFormat : "TYPE=GLOB" , description : "Modules import rule" , logValue : ( value ) => value . map ( ( rule ) => `{${ rule . type }: ${ rule . include . join ( ", " ) }}` ) . join ( ", " ) , fromEntries : ( entries ) => entries . map < ModuleRule > ( ( [ type , include ] ) => ( { type : type as ModuleRuleType , include : [ include ] , fallthrough : true , } ) ) , fromWrangler : ( { build } ) => build ?. upload ?. rules ?. map ( ( { type , globs , fallthrough } ) => ( { type , include : globs , fallthrough , } ) ) , } ) modulesRules ? : ModuleRule [ ] ; @ Option ( { type : OptionType . STRING , name : "compat-date" , description : "Opt into backwards-incompatible changes from" , fromWrangler : ( { compatibility_date } ) => compatibility_date , } ) compatibilityDate ? : string ; @ Option ( { type : OptionType . ARRAY , name : "compat-flag" , description : "Control specific backwards-incompatible changes" , fromWrangler : ( { compatibility_flags } ) => compatibility_flags , } ) compatibilityFlags ? : CompatibilityFlag [ ] ; @ Option ( { type : OptionType . STRING , alias : "u" , description : "URL of upstream origin" , fromWrangler : ( { miniflare } ) => miniflare ?. upstream , } ) upstream ? : string ; @ Option ( { type : OptionType . BOOLEAN , alias : "w" , description : "Watch files for changes" , fromWrangler : ( { miniflare } ) => miniflare ?. watch , } ) watch ? : boolean ; @ Option ( { type : OptionType . BOOLEAN , alias : "d" , description : "Enable debug logging" , } ) debug ? : boolean ; @ Option ( { type : OptionType . BOOLEAN , alias : "V" , description : "Enable verbose logging" , } ) verbose ? : boolean ; @ Option ( { type : OptionType . BOOLEAN , description : "Enable update checker (enabled by default)" , negatable : true , fromWrangler : ( { miniflare } ) => miniflare ?. update_check , } ) updateCheck ? : boolean ; @ Option ( { type : OptionType . STRING , name : "root" , description : "Path to resolve files relative to" , } ) rootPath ? : string ; @ Option ( { type : OptionType . OBJECT , typeFormat : "NAME=PATH[@ENV]" , description : "Mount additional named workers" , fromEntries : ( entries ) => Object . fromEntries ( entries . map ( ( entry ) => mapMountEntries ( entry ) ) ) , fromWrangler : ( { miniflare } , configDir ) => miniflare ?. mounts && Object . fromEntries ( Object . entries ( miniflare . mounts ) . map ( ( entry ) => mapMountEntries ( entry , configDir ) ) ) , } ) mounts ? : Record < string , string | CoreOptions | BindingsOptions > ; @ Option ( { type : OptionType . STRING , description : "Name of service" , fromWrangler : ( { name } ) => name , } ) name ? : string ; @ Option ( { type : OptionType . ARRAY , description : "Route to respond with this worker on" , fromWrangler : ( { route , routes , miniflare } ) => { const result = [ ] ; if ( route ) result . push ( route ) ; if ( routes ) result . push ( ... routes ) ; if ( miniflare ?. route ) result . push ( miniflare . route ) ; if ( miniflare ?. routes ) result . push ( ... miniflare . routes ) ; return result . length ? result : undefined ; } , } ) routes ? : string [ ] ; @ Option ( { type : OptionType . NONE } ) logUnhandledRejections ? : boolean ; @ Option ( { type : OptionType . BOOLEAN , name : "global-async-io" , description : "Allow async I/O outside handlers" , logName : "Allow Global Async I/O" , fromWrangler : ( { miniflare } ) => miniflare ?. global_async_io , } ) globalAsyncIO ? : boolean ; @ Option ( { type : OptionType . BOOLEAN , description : "Allow setting timers outside handlers" , logName : "Allow Global Timers" , fromWrangler : ( { miniflare } ) => miniflare ?. global_timers , } ) globalTimers ? : boolean ; @ Option ( { type : OptionType . BOOLEAN , description : "Allow secure random generation outside handlers" , logName : "Allow Global Secure Random" , fromWrangler : ( { miniflare } ) => miniflare ?. global_random , } ) globalRandom ? : boolean ; readonly processedModuleRules : ProcessedModuleRule [ ] = [ ] ; readonly upstreamURL ? : URL ; readonly #globals : Context ; constructor ( ctx , options ? ) { super ( ctx ) ; this . assignOptions ( options ) ; if ( this . mounts && Object . keys ( this . mounts ) . length ) { ctx . log . warn ( "Mounts are experimental. There may be breaking changes in the future." ) ; } let CompatRequest = Request ; let CompatResponse = Response ; const formDataFiles = ctx . compat . isEnabled ( "formdata_parser_supports_files" ) ; if ( ! formDataFiles ) { CompatRequest = proxyStringFormDataFiles ( CompatRequest ) ; CompatResponse = proxyStringFormDataFiles ( CompatResponse ) ; } try { this . upstreamURL = this . upstream === undefined ? undefined : new URL ( this . upstream ) ; } catch ( e ) { throw new MiniflareCoreError ( "ERR_INVALID_UPSTREAM" , `Invalid upstream URL: \"${ this . upstream }\". Make sure you've included the protocol.` ) ; } const blockGlobalTimers = ! this . globalTimers ; const crypto = createCrypto ( ! this . globalRandom ) ; this . #globals = { console , setTimeout : createTimer ( setTimeout , blockGlobalTimers ) , setInterval : createTimer ( setInterval , blockGlobalTimers ) , clearTimeout : assertsInRequest ( clearTimeout , blockGlobalTimers ) , clearInterval : assertsInRequest ( clearInterval , blockGlobalTimers ) , queueMicrotask , scheduler : new Scheduler ( blockGlobalTimers ) , atob , btoa , Math , crypto , CryptoKey : crypto . CryptoKey , TextDecoder , TextEncoder , fetch : createCompatFetch ( ctx ) , Headers , Request : CompatRequest , Response : CompatResponse , FormData , Blob , File , URL , URLSearchParams , ByteLengthQueuingStrategy , CountQueuingStrategy , ReadableByteStreamController , ReadableStream , ReadableStreamBYOBReader , ReadableStreamBYOBRequest , ReadableStreamDefaultController , ReadableStreamDefaultReader , TransformStream , TransformStreamDefaultController , WritableStream , WritableStreamDefaultController , WritableStreamDefaultWriter , FixedLengthStream , Event , EventTarget , AbortController , AbortSignal , FetchEvent , ScheduledEvent , DOMException , WorkerGlobalScope , structuredClone : globalThis . structuredClone ?? structuredCloneBuffer , ArrayBuffer , Atomics , BigInt64Array , BigUint64Array , DataView , Date , Float32Array , Float64Array , Int8Array , Int16Array , Int32Array , Map , Set , SharedArrayBuffer , Uint8Array , Uint8ClampedArray , Uint16Array , Uint32Array , WeakMap , WeakSet , WebAssembly , MINIFLARE : true , } ; if ( ! this . modules ) return ; const finalisedTypes = new Set < ModuleRuleType > ( ) ; for ( const rule of [ ... ( this . modulesRules ?? [ ] ) , ... DEFAULT_MODULE_RULES , ] ) { if ( finalisedTypes . has ( rule . type ) ) continue ; this . processedModuleRules . push ( { type : rule . type , include : globsToMatcher ( rule . include ) , } ) ; if ( ! rule . fallthrough ) finalisedTypes . add ( rule . type ) ; } } async setup ( ) { const globals = this . #globals ; if ( this . script !== undefined ) { return { globals , script : { filePath : STRING_SCRIPT_PATH , code : this . script } , } ; } const watch = [ ] ; let scriptPath = this . scriptPath ; if ( scriptPath === undefined ) { let packagePath = this . packagePath === true ? "package.json" : this . packagePath ; if ( packagePath ) { packagePath = path . resolve ( this . ctx . rootPath , packagePath ) ; try { const pkg = JSON . parse ( await fs . readFile ( packagePath , "utf8" ) ) ; scriptPath = this . modules ? pkg . module : pkg . main ; scriptPath &&= path . resolve ( path . dirname ( packagePath ) , scriptPath ) ; } catch ( e ) { if ( ! ( e . code === "ENOENT" && this . packagePath === true ) ) throw e ; } watch . push ( packagePath ) ; } } if ( scriptPath !== undefined ) { scriptPath = path . resolve ( this . ctx . rootPath , scriptPath ) ; const code = await fs . readFile ( scriptPath , "utf8" ) ; watch . push ( scriptPath ) ; return { globals , script : { filePath : scriptPath , code } , watch } ; } return { globals , watch } ; } }