Last active
June 2, 2024 05:42
-
-
Save easrng/9604bb71f6a49420c62c14218e76cdb3 to your computer and use it in GitHub Desktop.
cross-runtime http `serve` function for js
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
/// <reference lib="webworker" /> | |
// @ts-ignore | |
type _makeTSPlaygroundLoadTheBunTypes = import("bun-types"); | |
type ServeOptions = { | |
fetch: (req: Request) => PromiseLike<Response> | Response; | |
port?: number; | |
hostname?: string; | |
signal?: AbortSignal; | |
}; | |
type ServeAddress = { hostname: string; port: number; url: string }; | |
type ServeFunction = (options: ServeOptions) => Promise<ServeAddress>; | |
function getAddress(options: ServeOptions): ServeAddress { | |
const address: ServeAddress = { | |
port: options.port ?? 3000, | |
hostname: options.hostname ?? "::", | |
url: "", | |
}; | |
const url = new URL("http://x"); | |
url.hostname = address.hostname.includes(":") | |
? `[${address.hostname}]` | |
: address.hostname; | |
url.port = address.port.toString(); | |
if (url.hostname === "[::]") { | |
url.hostname = "localhost"; | |
address.hostname = "localhost"; | |
} | |
address.url = url.href; | |
return address; | |
} | |
let nodeServerPromise: Promise<typeof import("@hono/node-server")>; | |
type DenoType = { | |
serve( | |
options: { | |
port?: number; | |
hostname?: string; | |
signal?: AbortSignal; | |
reusePort?: boolean; | |
onError?: (error: unknown) => Response | Promise<Response>; | |
onListen?: (localAddr: unknown) => void; | |
}, | |
handler: (request: Request) => Response | Promise<Response>, | |
): unknown; | |
}; | |
const serveWeb: ServeFunction = async ({ fetch, signal }) => { | |
if (!signal || !signal.aborted) { | |
const handler = (event: FetchEvent) => { | |
event.respondWith(fetch(event.request)); | |
}; | |
globalThis.addEventListener("fetch", handler as any); | |
signal?.addEventListener( | |
"abort", | |
() => { | |
globalThis.removeEventListener("fetch", handler as any); | |
}, | |
{ once: true }, | |
); | |
} | |
return Promise.resolve( | |
typeof location !== "undefined" | |
? { | |
hostname: location.host, | |
port: parseInt(location.port), | |
url: location.origin, | |
} | |
: { | |
hostname: "unknown.invalid", | |
port: 0, | |
url: "http://unknown.invalid", | |
}, | |
); | |
}; | |
const serveBun: ServeFunction = async (options) => { | |
let Bun; | |
if ("Bun" in globalThis) { | |
Bun = (globalThis as any).Bun as typeof import("bun"); | |
} else { | |
throw new Error("not running in Bun"); | |
} | |
const address = getAddress(options); | |
if (!options.signal || !options.signal.aborted) { | |
const server = Bun.serve({ | |
fetch: async (req) => options.fetch(req), | |
hostname: address.hostname, | |
port: address.port, | |
}); | |
if (options.signal) { | |
options.signal.addEventListener("abort", () => server.stop(), { | |
once: true, | |
}); | |
} | |
} | |
return address; | |
}; | |
const serveNode: ServeFunction = async (options) => { | |
const address = getAddress(options); | |
if (!options.signal || !options.signal.aborted) { | |
const nodeServe = await (nodeServerPromise || | |
(nodeServerPromise = import("@hono/node-server"))); | |
if (!options.signal || !options.signal.aborted) { | |
let readyResolve: () => void; | |
const readyPromise = new Promise<void>( | |
(resolve) => (readyResolve = resolve), | |
); | |
const server = nodeServe.serve( | |
{ | |
fetch: options.fetch, | |
port: address.port, | |
hostname: address.hostname, | |
}, | |
() => { | |
readyResolve(); | |
}, | |
); | |
if (options.signal) { | |
options.signal.addEventListener("abort", () => server.close(), { | |
once: true, | |
}); | |
} | |
await readyPromise; | |
} | |
} | |
return address; | |
}; | |
const serveDeno: ServeFunction = async (options) => { | |
let Deno; | |
if ("Deno" in globalThis) { | |
Deno = (globalThis as any).Deno as DenoType; | |
} else { | |
throw new Error("not running in Deno"); | |
} | |
const address = getAddress(options); | |
if (!options.signal || !options.signal.aborted) { | |
let readyResolve: () => void; | |
const readyPromise = new Promise<void>( | |
(resolve) => (readyResolve = resolve), | |
); | |
Deno.serve( | |
{ | |
hostname: address.hostname, | |
port: address.port, | |
signal: options.signal, | |
onListen() { | |
readyResolve(); | |
}, | |
}, | |
async (req) => options.fetch(req), | |
); | |
await readyPromise; | |
} | |
return address; | |
}; | |
const serveAuto: ServeFunction = (options) => { | |
if ("Bun" in globalThis) { | |
return serveBun(options); | |
} else if ("Deno" in globalThis) { | |
return serveDeno(options); | |
} else if (typeof process !== "undefined") { | |
return serveNode(options); | |
} else if ( | |
typeof ServiceWorkerGlobalScope !== "undefined" && | |
globalThis instanceof ServiceWorkerGlobalScope | |
) { | |
return serveWeb(options); | |
} else { | |
throw new Error("Unsupported runtime."); | |
} | |
}; | |
export { | |
serveWeb, | |
serveNode, | |
serveBun, | |
serveDeno, | |
serveAuto, | |
serveAuto as default, | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment