|
| 1 | +import fs from "fs/promises"; |
| 2 | +import { Request, Response } from "undici"; |
| 3 | +import { z } from "zod"; |
| 4 | +import { Awaitable, JsonSchema } from "../../helpers"; |
| 5 | +import { Service, Worker, Worker_Binding } from "../../runtime"; |
| 6 | +import { BINDING_SERVICE_LOOPBACK, Plugin } from "../shared"; |
| 7 | + |
| 8 | +// (request: Request) => Awaitable<Response> |
| 9 | +export const ServiceFetch = z |
| 10 | + .function() |
| 11 | + .args(z.instanceof(Request)) |
| 12 | + .returns(z.instanceof(Response).or(z.promise(z.instanceof(Response)))); |
| 13 | + |
| 14 | +export const CoreOptionsSchema = z.object({ |
| 15 | + name: z.string().optional(), |
| 16 | + script: z.string().optional(), |
| 17 | + scriptPath: z.string().optional(), |
| 18 | + modules: z.boolean().optional(), |
| 19 | + compatibilityDate: z.string().optional(), |
| 20 | + compatibilityFlags: z.string().array().optional(), |
| 21 | + |
| 22 | + bindings: z.record(JsonSchema).optional(), |
| 23 | + wasmBindings: z.record(z.string()).optional(), |
| 24 | + textBlobBindings: z.record(z.string()).optional(), |
| 25 | + dataBlobBindings: z.record(z.string()).optional(), |
| 26 | + serviceBindings: z.record(z.union([z.string(), ServiceFetch])).optional(), |
| 27 | +}); |
| 28 | +export const CoreSharedOptionsSchema = z.object({ |
| 29 | + host: z.string().optional(), |
| 30 | + port: z.number().optional(), |
| 31 | +}); |
| 32 | + |
| 33 | +export const CORE_PLUGIN_NAME = "core"; |
| 34 | + |
| 35 | +// Service looping back to Miniflare's Node.js process (for storage, etc) |
| 36 | +export const SERVICE_LOOPBACK = `${CORE_PLUGIN_NAME}:loopback`; |
| 37 | +// Service for HTTP socket entrypoint (for checking runtime ready, routing, etc) |
| 38 | +export const SERVICE_ENTRY = `${CORE_PLUGIN_NAME}:entry`; |
| 39 | +// Service prefix for all regular user workers |
| 40 | +const SERVICE_USER_PREFIX = `${CORE_PLUGIN_NAME}:user`; |
| 41 | +// Service prefix for custom fetch functions defined in `serviceBindings` option |
| 42 | +const SERVICE_CUSTOM_PREFIX = `${CORE_PLUGIN_NAME}:custom`; |
| 43 | + |
| 44 | +export const HEADER_PROBE = "MF-Probe"; |
| 45 | +export const HEADER_CUSTOM_SERVICE = "MF-Custom-Service"; |
| 46 | + |
| 47 | +const BINDING_JSON_VERSION = "MINIFLARE_VERSION"; |
| 48 | +const BINDING_SERVICE_USER = "MINIFLARE_USER"; |
| 49 | +const BINDING_TEXT_CUSTOM_SERVICE = "MINIFLARE_CUSTOM_SERVICE"; |
| 50 | + |
| 51 | +// TODO: is there a way of capturing the full stack trace somehow? |
| 52 | +// Using `>=` for version check to handle multiple `setOptions` calls before |
| 53 | +// reload complete. |
| 54 | +export const SCRIPT_ENTRY = `addEventListener("fetch", (event) => { |
| 55 | + const probe = event.request.headers.get("${HEADER_PROBE}"); |
| 56 | + if (probe !== null) { |
| 57 | + const probeMin = parseInt(probe); |
| 58 | + const status = ${BINDING_JSON_VERSION} >= probeMin ? 204 : 412; |
| 59 | + return event.respondWith(new Response(null, { status })); |
| 60 | + } |
| 61 | +
|
| 62 | + if (globalThis.${BINDING_SERVICE_USER} !== undefined) { |
| 63 | + event.respondWith(${BINDING_SERVICE_USER}.fetch(event.request).catch((err) => new Response(err.stack))); |
| 64 | + } else { |
| 65 | + event.respondWith(new Response("No script! 😠", { status: 404 })); |
| 66 | + } |
| 67 | +});`; |
| 68 | +export const SCRIPT_CUSTOM_SERVICE = `addEventListener("fetch", (event) => { |
| 69 | + const request = new Request(event.request); |
| 70 | + request.headers.set("${HEADER_CUSTOM_SERVICE}", ${BINDING_TEXT_CUSTOM_SERVICE}); |
| 71 | + event.respondWith(${BINDING_SERVICE_LOOPBACK}.fetch(request)); |
| 72 | +})`; |
| 73 | + |
| 74 | +export const CORE_PLUGIN: Plugin< |
| 75 | + typeof CoreOptionsSchema, |
| 76 | + typeof CoreSharedOptionsSchema |
| 77 | +> = { |
| 78 | + options: CoreOptionsSchema, |
| 79 | + sharedOptions: CoreSharedOptionsSchema, |
| 80 | + getBindings(options) { |
| 81 | + const bindings: Awaitable<Worker_Binding>[] = []; |
| 82 | + |
| 83 | + if (options.bindings !== undefined) { |
| 84 | + bindings.push( |
| 85 | + ...Object.entries(options.bindings).map(([name, value]) => ({ |
| 86 | + name, |
| 87 | + json: JSON.stringify(value), |
| 88 | + })) |
| 89 | + ); |
| 90 | + } |
| 91 | + if (options.wasmBindings !== undefined) { |
| 92 | + bindings.push( |
| 93 | + ...Object.entries(options.wasmBindings).map(([name, path]) => |
| 94 | + fs.readFile(path).then((wasmModule) => ({ name, wasmModule })) |
| 95 | + ) |
| 96 | + ); |
| 97 | + } |
| 98 | + if (options.textBlobBindings !== undefined) { |
| 99 | + bindings.push( |
| 100 | + ...Object.entries(options.textBlobBindings).map(([name, path]) => |
| 101 | + fs.readFile(path, "utf8").then((text) => ({ name, text })) |
| 102 | + ) |
| 103 | + ); |
| 104 | + } |
| 105 | + if (options.dataBlobBindings !== undefined) { |
| 106 | + bindings.push( |
| 107 | + ...Object.entries(options.dataBlobBindings).map(([name, path]) => |
| 108 | + fs.readFile(path).then((data) => ({ name, data })) |
| 109 | + ) |
| 110 | + ); |
| 111 | + } |
| 112 | + if (options.serviceBindings !== undefined) { |
| 113 | + bindings.push( |
| 114 | + ...Object.entries(options.serviceBindings).map(([name, service]) => ({ |
| 115 | + name, |
| 116 | + service: { |
| 117 | + name: |
| 118 | + typeof service === "function" |
| 119 | + ? `${SERVICE_CUSTOM_PREFIX}:${name}` // Custom `fetch` function |
| 120 | + : `${SERVICE_USER_PREFIX}:${name}`, // Regular user worker |
| 121 | + }, |
| 122 | + })) |
| 123 | + ); |
| 124 | + } |
| 125 | + |
| 126 | + return Promise.all(bindings); |
| 127 | + }, |
| 128 | + async getServices({ options, optionsVersion, workerBindings, workerIndex }) { |
| 129 | + // Define core/shared services. |
| 130 | + // Services get de-duped by name, so only the first worker's |
| 131 | + // SERVICE_LOOPBACK and SERVICE_ENTRY will be used |
| 132 | + const serviceEntryBindings: Worker_Binding[] = [ |
| 133 | + { name: BINDING_JSON_VERSION, json: optionsVersion.toString() }, |
| 134 | + ]; |
| 135 | + const services: Service[] = [ |
| 136 | + { name: SERVICE_LOOPBACK, external: { http: {} } }, |
| 137 | + { |
| 138 | + name: SERVICE_ENTRY, |
| 139 | + worker: { |
| 140 | + serviceWorkerScript: SCRIPT_ENTRY, |
| 141 | + bindings: serviceEntryBindings, |
| 142 | + }, |
| 143 | + }, |
| 144 | + ]; |
| 145 | + |
| 146 | + // Define regular user worker if script is set |
| 147 | + let workerScript: Partial<Worker> | undefined; |
| 148 | + if (options.script !== undefined) { |
| 149 | + workerScript = options.modules |
| 150 | + ? { modules: [{ name: "<script>", esModule: options.script }] } |
| 151 | + : { serviceWorkerScript: options.script }; |
| 152 | + } else if (options.scriptPath !== undefined) { |
| 153 | + if (options.modules) { |
| 154 | + // TODO: collect modules |
| 155 | + } else { |
| 156 | + const script = await fs.readFile(options.scriptPath, "utf8"); |
| 157 | + workerScript = { serviceWorkerScript: script }; |
| 158 | + } |
| 159 | + } |
| 160 | + |
| 161 | + if (workerScript !== undefined) { |
| 162 | + const name = `${SERVICE_USER_PREFIX}:${options.name ?? ""}`; |
| 163 | + services.push({ |
| 164 | + name, |
| 165 | + worker: { |
| 166 | + ...workerScript, |
| 167 | + compatibilityDate: options.compatibilityDate, |
| 168 | + compatibilityFlags: options.compatibilityFlags, |
| 169 | + bindings: workerBindings, |
| 170 | + }, |
| 171 | + }); |
| 172 | + serviceEntryBindings.push({ |
| 173 | + name: BINDING_SERVICE_USER, |
| 174 | + service: { name }, |
| 175 | + }); |
| 176 | + } |
| 177 | + |
| 178 | + // Define custom `fetch` services if set |
| 179 | + if (options.serviceBindings !== undefined) { |
| 180 | + for (const [name, service] of Object.entries(options.serviceBindings)) { |
| 181 | + if (typeof service === "function") { |
| 182 | + services.push({ |
| 183 | + name: `${SERVICE_CUSTOM_PREFIX}:${name}`, |
| 184 | + worker: { |
| 185 | + serviceWorkerScript: SCRIPT_CUSTOM_SERVICE, |
| 186 | + bindings: [ |
| 187 | + { |
| 188 | + name: BINDING_TEXT_CUSTOM_SERVICE, |
| 189 | + text: `${workerIndex}/${name}`, |
| 190 | + }, |
| 191 | + { |
| 192 | + name: BINDING_SERVICE_LOOPBACK, |
| 193 | + service: { name: SERVICE_LOOPBACK }, |
| 194 | + }, |
| 195 | + ], |
| 196 | + }, |
| 197 | + }); |
| 198 | + } |
| 199 | + } |
| 200 | + } |
| 201 | + |
| 202 | + return services; |
| 203 | + }, |
| 204 | +}; |
0 commit comments