diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 667b85b..9043c27 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -25,15 +25,11 @@ jobs: run: npm ci env: CI: true - - name: build - run: npm run build + - name: prepare + run: npm run prepare env: CI: true - name: vitest run: npm run test env: CI: true - - name: deno lint - run: | - cd deno-bootstrap - deno lint diff --git a/README.md b/README.md new file mode 100644 index 0000000..9e1b6ef --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# deno-http-worker + +[![NPM version](https://img.shields.io/npm/v/deno-http-worker.svg?style=flat)](https://npmjs.org/package/deno-http-worker) + +Similarly to [deno-vm](https://github.com/casual-simulation/node-deno-vm), deno-http-worker lets you securely spawn Deno http servers. + +## Usage + +```ts +import { newDenoHTTPWorker } from 'deno-http-worker'; + +let worker = await newDenoHTTPWorker( + `export default async function (req: Request): Promise { + return Response.json({ ok: req.url }) + }`, + { printOutput: true, runFlags: ["--alow-net"] } +); + +let json = await worker.client + .get("https://hello/world?query=param") + .json(); +console.log(json) // => { ok: 'https://hello/world?query=param' } + +worker.terminate(); +``` + +## Internals + +Deno-http-worker connects to the Deno process over a single Unix socket http2 connection to make requests. This is for performance and efficiency. As a result, the worker does not provide an address or url, but instead returns an instance of a [got](https://www.npmjs.com/package/got) client that you can make requests with. This ensures that only the underlying `http2.ClientHttp2Session` is used to make requests. + +If you need more advanced usage that cannot be covered by `got`, please open a ticket. \ No newline at end of file diff --git a/deno-bootstrap/index.ts b/deno-bootstrap/index.ts index f4d7ab3..0960d39 100644 --- a/deno-bootstrap/index.ts +++ b/deno-bootstrap/index.ts @@ -1,23 +1,12 @@ -const scriptType = Deno.args[0]; -const script = Deno.args[1]; +const socketFile = Deno.args[0]; +const scriptType = Deno.args[1]; +const script = Deno.args[2]; const importURL = scriptType == "import" ? script : "data:text/tsx," + encodeURIComponent(script); -const server = Deno.listen({ - hostname: "0.0.0.0", - port: 0, -}); - -const addr = server.addr as Deno.NetAddr; - -console.log(`deno-listening-port ${addr.port.toString().padStart(5, " ")} `); - -// Now that we're listening, start executing user-provided code. We could -// import while starting the server for a small performance improvement, -// but it would complicate reading the port from the Deno logs. const handler = await import(importURL); if (!handler.default) { throw new Error("No default export found in script."); @@ -26,32 +15,22 @@ if (typeof handler.default !== "function") { throw new Error("Default export is not a function."); } -const conn = await server.accept(); -(async () => { - // Reject all additional connections. - for await (const conn of server) { - conn.close(); - } -})(); - -// serveHttp is deprecated, but we don't have many other options if we'd like to -// keep this pattern of rejecting future connections at the TCP level. -// https://discord.com/channels/684898665143206084/1232398264947445810/1234614780111880303 -// -// deno-lint-ignore no-deprecated-deno-api -const httpConn = Deno.serveHttp(conn); -for await (const requestEvent of httpConn) { - (async () => { - let req = requestEvent.request; - const url = new URL(req.url); - url.host = req.headers.get("X-Deno-Worker-Host") || url.host; - url.protocol = req.headers.get("X-Deno-Worker-Protocol") + ":"; - url.port = req.headers.get("X-Deno-Worker-Port") || url.port; - req = new Request(url.toString(), req); - req.headers.delete("X-Deno-Worker-Host"); - req.headers.delete("X-Deno-Worker-Protocol"); - req.headers.delete("X-Deno-Worker-Port"); - - await requestEvent.respondWith(handler.default(req)); - })(); -} +// Use an empty onListen callback to prevent Deno from logging +Deno.serve({ path: socketFile, onListen: () => {} }, (req: Request) => { + const url = new URL(req.url); + url.host = req.headers.get("X-Deno-Worker-Host") || url.host; + url.port = req.headers.get("X-Deno-Worker-Port") || url.port; + // Setting url.protocol did not replace the protocol correctly for a unix + // socket. Replacing the href value seems to work well. + url.href = url.href.replace( + /^http\+unix:/, + req.headers.get("X-Deno-Worker-Protocol") || url.protocol + ); + // Deno Request headers are immutable so we must make a new Request in order to delete our headers + req = new Request(url.toString(), req); + req.headers.delete("X-Deno-Worker-Host"); + req.headers.delete("X-Deno-Worker-Protocol"); + req.headers.delete("X-Deno-Worker-Port"); + + return handler.default(req); +}); diff --git a/package.json b/package.json index 60ba145..2f964cc 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,17 @@ { "name": "deno-http-worker", - "version": "0.0.0", + "version": "0.0.2", "description": "", - "main": "dist/plugin.js", - "types": "./dist/plugin.d.ts", + "main": "dist/index.js", + "types": "./dist/index.d.ts", "scripts": { "test": "vitest run", "test:watch": "vitest", + "lint" : "npm run lint:deno-bootstrap && npm run lint:deno-test-files", + "lint:deno-bootstrap": "cd deno-bootstrap && deno lint", + "lint:deno-test-files": "cd src/test && deno lint", "build": "tsc --build", - "prepare": "npm run build" + "prepare": "npm run lint && npm run build" }, "repository": { "type": "git", diff --git a/src/DenoHTTPWorker.test.ts b/src/DenoHTTPWorker.test.ts index 4048472..9934b98 100644 --- a/src/DenoHTTPWorker.test.ts +++ b/src/DenoHTTPWorker.test.ts @@ -5,7 +5,7 @@ import path from "path"; // Uncomment this if you want to debug serial test execution const it = _it.concurrent; -// const it = _it +// const it = _it; describe("DenoHTTPWorker", { timeout: 1000 }, () => { const echoFile = path.resolve(__dirname, "./test/echo-request.ts"); @@ -14,7 +14,8 @@ describe("DenoHTTPWorker", { timeout: 1000 }, () => { const vtScript = fs.readFileSync(vtFile, { encoding: "utf-8" }); it("json response multiple requests", async () => { - let worker = await newDenoHTTPWorker(` + let worker = await newDenoHTTPWorker( + ` export default async function (req: Request): Promise { let headers = {}; for (let [key, value] of req.headers.entries()) { @@ -22,13 +23,15 @@ describe("DenoHTTPWorker", { timeout: 1000 }, () => { } return Response.json({ ok: req.url, headers: headers }) } - `); + `, + { printOutput: true } + ); for (let i = 0; i < 10; i++) { let json = await worker.client - .get("https://localhost/", { headers: {} }) + .get("https://localhost/hello?isee=you", { headers: {} }) .json(); expect(json).toEqual({ - ok: "https://localhost/", + ok: "https://localhost/hello?isee=you", headers: { accept: "application/json", "accept-encoding": "gzip, deflate, br", @@ -38,27 +41,30 @@ describe("DenoHTTPWorker", { timeout: 1000 }, () => { worker.terminate(); }); - it("deny-net not always allowed", async () => { - expect( - newDenoHTTPWorker(echoScript, { - runFlags: [`--deny-net`], + describe("runFlags editing", () => { + it.each([ + "--allow-read", + "--allow-write", + "--allow-read=/dev/null", + "--allow-write=/dev/null", + "--allow-read=foo,/dev/null", + "--allow-write=bar,/dev/null", + ])("should handle %s", async (flag) => { + let worker = await newDenoHTTPWorker(echoScript, { printOutput: true, - }) - ).rejects.toThrowError("not supported"); - expect( - newDenoHTTPWorker(echoScript, { - runFlags: [`--deny-net=0.0.0.0:0`], - printOutput: true, - }) - ).rejects.toThrowError("with the address"); + runFlags: [flag], + }); + await worker.client.get("https://localhost/").json(); + await worker.terminate(); + }); }); it("should be able to import script", async () => { const file = path.resolve(__dirname, "./test/echo-request.ts"); const url = new URL(`file://${file}`); let worker = await newDenoHTTPWorker(url, { - runFlags: [`--allow-read=${file}`], printOutput: true, + printCommandAndArguments: true, }); let resp: any = await worker.client @@ -70,7 +76,9 @@ describe("DenoHTTPWorker", { timeout: 1000 }, () => { }); it("user agent is not overwritten", async () => { - let worker = await newDenoHTTPWorker(echoScript); + let worker = await newDenoHTTPWorker(echoScript, { + printOutput: true, + }); let resp: any = await worker.client .get("https://localhost/", { headers: { "User-Agent": "some value" }, @@ -116,41 +124,6 @@ describe("DenoHTTPWorker", { timeout: 1000 }, () => { worker.terminate(); }); - it("port log is not in output", async () => { - let worker = await newDenoHTTPWorker( - `console.log("Hi, I am here"); - export default async function (req: Request): Promise { - let body = await req.text(); - return Response.json({ length: body.length }) - }` - ); - let allStdout = ""; - - worker.stdout.on("data", (data) => { - allStdout += data; - }); - - await worker.client("https://hey.ho").text(); - worker.terminate(); - - expect(allStdout).toEqual("Hi, I am here\n"); - }); - - it("cannot make outside connection to deno server", async () => { - let worker = await newDenoHTTPWorker( - `export default async function (req: Request): Promise { - let body = await req.text(); - return Response.json({ length: body.length }) - }` - ); - - await expect( - fetch("http://localhost:" + worker.denoListeningPort) - ).rejects.toThrowError("fetch failed"); - - worker.terminate(); - }); - it("can implement val town", async () => { let worker = await newDenoHTTPWorker(vtScript, { printOutput: true }); @@ -158,13 +131,14 @@ describe("DenoHTTPWorker", { timeout: 1000 }, () => { body: "data:text/tsx," + encodeURIComponent(`export default async function (req: Request): Promise { - return Response.json({ ok: true }) - } ${"///".repeat(8000)}`), + return Response.json({ ok: true }) + } ${"///".repeat(8000)}`), }); // We send a request to initialize and when the first request is in flight // we send another request let second = worker.client("https://foo.web.val.run"); + expect((await first).statusCode).toEqual(200); await first.text(); expect(await second.text()).toEqual('{"ok":true}'); diff --git a/src/DenoHTTPWorker.ts b/src/DenoHTTPWorker.ts index 87311bd..0b5f791 100644 --- a/src/DenoHTTPWorker.ts +++ b/src/DenoHTTPWorker.ts @@ -4,6 +4,8 @@ import { Readable, Writable, TransformCallback, Transform } from "stream"; import readline from "readline"; import http2 from "http2-wrapper"; import got, { Got } from "got"; +import fs from "fs/promises"; +import net from "net"; import { fileURLToPath } from "url"; @@ -20,10 +22,9 @@ const DEFAULT_DENO_BOOTSTRAP_SCRIPT_PATH = __dirname.endsWith("src") export interface DenoWorkerOptions { /** * The path to the executable that should be use when spawning the subprocess. - * Defaults to "deno". You can pass an array here if you want to invoke Deno - * with multiple arguments, like `sandbox run deno`. + * Defaults to "deno". */ - denoExecutable: string | string[]; + denoExecutable: string; /** * The path to the script that should be used to bootstrap the worker @@ -64,70 +65,64 @@ export interface DenoWorkerOptions { export const newDenoHTTPWorker = async ( script: string | URL, - options?: Partial + options: Partial = {} ): Promise => { - const _options: DenoWorkerOptions = Object.assign( - { - denoExecutable: "deno", - denoBootstrapScriptPath: DEFAULT_DENO_BOOTSTRAP_SCRIPT_PATH, - runFlags: [], - printCommandAndArguments: false, - spawnOptions: {}, - printOutput: false, - }, - options || {} - ); - - let networkingIsOk = false; + const _options: DenoWorkerOptions = { + denoExecutable: "deno", + denoBootstrapScriptPath: DEFAULT_DENO_BOOTSTRAP_SCRIPT_PATH, + runFlags: [], + printCommandAndArguments: false, + spawnOptions: {}, + printOutput: false, + ...options, + }; + + let scriptArgs: string[]; + + // Create the socket location that we'll use to communicate with Deno. + const socketFile = `${crypto.randomUUID()}-deno-http.sock`; + + // If we have a file import, make sure we allow read access to the file. + const allowReadFlagValue = + typeof script === "string" + ? socketFile + : `${socketFile},${script.href.replace("file://", "")}`; + + let allowReadFound = false; + let allowWriteFound = false; _options.runFlags = _options.runFlags.map((flag) => { - if (flag === "--allow-net" || flag === "--allow-all") { - networkingIsOk = true; + if (flag === "--allow-read" || flag === "--allow-all") { + allowReadFound = true; } - if (flag === "--deny-net") { - throw new Error( - "Using --deny-net without specifying specific addresses is not supported" - ); + if (flag === "--allow-write" || flag === "--allow-all") { + allowWriteFound = true; } - if (flag.startsWith("--deny-net") && flag.includes(LISTENING_HOSTPORT)) { - throw new Error( - `Using --deny-net with the address ${LISTENING_HOSTPORT} is not supported` - ); + if (flag.startsWith("--allow-read=")) { + allowReadFound = true; + return (flag += "," + allowReadFlagValue); } - if (flag.startsWith("--allow-net=")) { - networkingIsOk = true; - return (flag += "," + LISTENING_HOSTPORT); + if (flag.startsWith("--allow-write=")) { + allowWriteFound = true; + return (flag += "," + socketFile); } return flag; }); - if (!networkingIsOk) { - _options.runFlags.push("--allow-net=" + LISTENING_HOSTPORT); + if (!allowReadFound) { + _options.runFlags.push("--allow-read=" + allowReadFlagValue); + } + if (!allowWriteFound) { + _options.runFlags.push("--allow-write=" + socketFile); } - - let scriptArgs: string[]; if (typeof script === "string") { - scriptArgs = ["script", script]; + scriptArgs = [socketFile, "script", script]; } else { - scriptArgs = ["import", script.href]; + scriptArgs = [socketFile, "import", script.href]; } - let command = "deno"; - if (typeof _options.denoExecutable === "string") { - command = _options.denoExecutable; - } - - if (Array.isArray(_options.denoExecutable)) { - if (_options.denoExecutable.length === 0) - throw new Error("denoExecutable must not be empty"); + const command = _options.denoExecutable; - command = _options.denoExecutable[0]!; - _options.runFlags = [ - ..._options.denoExecutable.slice(1), - ..._options.runFlags, - ]; - } - - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { const args = [ "run", ..._options.runFlags, @@ -135,12 +130,14 @@ export const newDenoHTTPWorker = async ( ...scriptArgs, ]; if (_options.printCommandAndArguments) { - console.log("Spawning deno process:", JSON.stringify([command, ...args])); + console.log("Spawning deno process:", [command, ...args]); } const process = spawn(command, args, _options.spawnOptions); let running = false; + let exited = false; let worker: DenoHTTPWorker; process.on("exit", (code: number, signal: string) => { + exited = true; if (!running) { let stderr = process.stderr?.read()?.toString(); reject( @@ -149,6 +146,7 @@ export const newDenoHTTPWorker = async ( (stderr ? `\n${stderr}` : "") ) ); + fs.rm(socketFile); } else { worker.terminate(code, signal); } @@ -157,113 +155,100 @@ export const newDenoHTTPWorker = async ( const stdout = process.stdout; const stderr = process.stderr; - const onReadable = () => { - // We wait for stdout to be readable and then just read the bytes of the - // port number log line. If a user subscribes to the reader later they'll - // only see log output without the port line. + if (_options.printOutput) { + readline.createInterface({ input: stdout }).on("line", (line) => { + console.log("[deno]", line); + }); + readline.createInterface({ input: stderr }).on("line", (line) => { + console.error("[deno]", line); + }); + } - // Length is: DENO_PORT_LOG_PREFIX + " " + port + padding + " " + newline - let data = stdout.read(DENO_PORT_LOG_PREFIX.length + 1 + 5 + 1 + 1); - stdout.removeListener("readable", onReadable); - let strData = data.toString(); - if (!strData.startsWith(DENO_PORT_LOG_PREFIX)) { - reject( - new Error( - "First log output from deno process did not contain the expected port value" - ) - ); - return; + // Wait for the socket file to be created by the Deno process. + while (true) { + if (exited) { + break; + } + try { + await fs.stat(socketFile); + // File exists + break; + } catch (err) { + await new Promise((resolve) => setTimeout(resolve, 50)); } + } - const match = strData.match(/deno-listening-port +(\d+) /); - if (!match) { - reject( - new Error( - `First log output from deno process did not contain a valid port value: "${data}"` - ) - ); - return; + const _httpSession = http2.connect(`http://whatever`, { + createConnection: () => net.connect(socketFile), + }); + _httpSession.on("error", (err) => { + if (!running) { + reject(err); + } else { + worker.terminate(); + throw err; } - const port = match[1]; - const _httpSession = http2.connect(`http://localhost:${port}`); - _httpSession.on("error", (err) => { - if (!running) { - reject(err); - } else { - worker.terminate(); - throw err; - } - }); - _httpSession.on("connect", () => { - const _got = got.extend({ - hooks: { - beforeRequest: [ - (options) => { - // Ensure that we use our existing session - options.h2session = _httpSession; - options.http2 = true; - - // We follow got's example here: - // https://github.com/sindresorhus/got/blob/88e623a0d8140e02eef44d784f8d0327118548bc/documentation/examples/h2c.js#L32-L34 - // But, this still surfaces a type error for various - // differences between the implementation. Ignoring for now. - // - // @ts-ignore - options.request = http2.request; - - // Ensure the got user-agent string is never present. If a - // value is passed by the user it will override got's - // default value. - if ( - options.headers["user-agent"] === - "got (https://github.com/sindresorhus/got)" - ) { - delete options.headers["user-agent"]; - } - - // Got will block requests that have a scheme of https and - // will also add a :443 port when not port exists. We pass - // the parts of the url that we care about in headers so - // that we can successfully assemble the request on the - // other side. - if (typeof options.url === "string") { - options.url = new URL(options.url); - } - options.headers = { - ...options.headers, - "X-Deno-Worker-Host": options.url?.host, - "X-Deno-Worker-Port": options.url?.port, - "X-Deno-Worker-Protocol": options.url?.protocol, - }; - if (options.url && options.url?.protocol === "https:") { - options.url.protocol = "http:"; - } - }, - ], - }, - }); - if (_options.printOutput) { - readline.createInterface({ input: stdout }).on("line", (line) => { - console.log("[deno]", line); - }); - readline.createInterface({ input: stderr }).on("line", (line) => { - console.error("[deno]", line); - }); - } - - worker = new DenoHTTPWorker( - _httpSession, - port, - _got, - process, - stdout, - stderr - ); - running = true; - resolve(worker); + }); + _httpSession.on("connect", () => { + const _got = got.extend({ + hooks: { + beforeRequest: [ + (options) => { + // Ensure that we use our existing session + options.h2session = _httpSession; + options.http2 = true; + + // We follow Got's example here: + // https://github.com/sindresorhus/got/blob/88e623a0d8140e02eef44d784f8d0327118548bc/documentation/examples/h2c.js#L32-L34 + // But, this still surfaces a type error for various + // differences between the implementation. Ignoring for now. + // + // @ts-ignore + options.request = http2.request; + + // Ensure the Got user-agent string is never present. If a + // value is passed by the user it will override got's + // default value. + if ( + options.headers["user-agent"] === + "got (https://github.com/sindresorhus/got)" + ) { + delete options.headers["user-agent"]; + } + + // Got will block requests that have a scheme of https and + // will also add a :443 port when not port exists. We pass + // the parts of the url that we care about in headers so + // that we can successfully assemble the request on the + // other side. + if (typeof options.url === "string") { + options.url = new URL(options.url); + } + options.headers = { + ...options.headers, + "X-Deno-Worker-Host": options.url?.host, + "X-Deno-Worker-Port": options.url?.port, + "X-Deno-Worker-Protocol": options.url?.protocol, + }; + if (options.url && options.url?.protocol === "https:") { + options.url.protocol = "http:"; + } + }, + ], + }, }); - }; - stdout.on("readable", onReadable); + + worker = new DenoHTTPWorker( + _httpSession, + socketFile, + _got, + process, + stdout, + stderr + ); + running = true; + resolve(worker); + }); }); }; export type { DenoHTTPWorker }; @@ -271,7 +256,7 @@ export type { DenoHTTPWorker }; class DenoHTTPWorker { #httpSession: http2.ClientHttp2Session; #got: Got; - #denoListeningPort: number; + #socketFile: string; #process: ChildProcess; #stdout: Readable; #stderr: Readable; @@ -279,14 +264,14 @@ class DenoHTTPWorker { constructor( httpSession: http2.ClientHttp2Session, - denoListeningPort: number, + socketFile: string, got: Got, process: ChildProcess, stdout: Readable, stderr: Readable ) { this.#httpSession = httpSession; - this.#denoListeningPort = denoListeningPort; + this.#socketFile = socketFile; this.#got = got; this.#process = process; this.#stdout = stdout; @@ -296,16 +281,19 @@ class DenoHTTPWorker { get client(): Got { return this.#got; } + terminate(code?: number, signal?: string) { if (this.#terminated) { return; } - this.onexit(code || this.#process.exitCode || 0, signal || ""); this.#terminated = true; + this.onexit(code || this.#process.exitCode || 0, signal || ""); if (this.#process && this.#process.exitCode === null) { - // TODO: is this preventing listening on SIGINT for cleanup? Do we care? + // TODO: do we need to SIGINT first to make sure we allow the process to do + // any cleanup? forceKill(this.#process.pid!); } + fs.rm(this.#socketFile); this.#httpSession.close(); } @@ -317,9 +305,6 @@ class DenoHTTPWorker { return this.#stderr; } - get denoListeningPort(): number { - return this.#denoListeningPort; - } /** * Represents an event handler for the "exit" event. That is, a function to be * called when the Deno worker process is terminated. @@ -327,38 +312,12 @@ class DenoHTTPWorker { onexit: (code: number, signal: string) => void = () => {}; } -function addOption( - list: string[], - name: string, - option: boolean | string[] | undefined -) { - if (option === true) { - list.push(`${name}`); - } else if (Array.isArray(option)) { - let values = option.join(","); - list.push(`${name}=${values}`); - } -} - /** * Forcefully kills the process with the given ID. * On Linux/Unix, this means sending the process the SIGKILL signal. - * On Windows, this means using the taskkill executable to kill the process. - * @param pid The ID of the process to kill. */ export function forceKill(pid: number) { - // TODO: do we need to SIGINT first to make sure we allow the process to do - // any cleanup? - const isWindows = /^win/.test(process.platform); - if (isWindows) { - return killWindows(pid); - } else { - return killUnix(pid); - } -} - -function killWindows(pid: number) { - execSync(`taskkill /PID ${pid} /T /F`); + return killUnix(pid); } function killUnix(pid: number) { diff --git a/src/test/deno.json b/src/test/deno.json new file mode 100644 index 0000000..09b20b0 --- /dev/null +++ b/src/test/deno.json @@ -0,0 +1 @@ +{ "lock": false } diff --git a/src/test/echo-request.ts b/src/test/echo-request.ts index 2204eb8..3778991 100644 --- a/src/test/echo-request.ts +++ b/src/test/echo-request.ts @@ -1,6 +1,6 @@ export default async function (req: Request): Promise { - let headers: { [key: string]: string } = {}; - for (let [key, value] of req.headers.entries()) { + const headers: { [key: string]: string } = {}; + for (const [key, value] of req.headers.entries()) { headers[key] = value; } return Response.json({ diff --git a/src/test/val-town.ts b/src/test/val-town.ts index c1d45f3..074d4f9 100644 --- a/src/test/val-town.ts +++ b/src/test/val-town.ts @@ -2,7 +2,11 @@ let initialized = false; let initializing = false; let handler: (req: Request) => Promise | Response; -let pendingRequests: any[] = []; +const pendingRequests: { + req: Request; + resolve: (value: Response | Promise) => void; + reject: (reason?: unknown) => void; +}[] = []; export default async function (req: Request): Promise { if (initializing) { return new Promise((resolve, reject) => { @@ -12,17 +16,18 @@ export default async function (req: Request): Promise { if (!initialized) { initializing = true; try { - let source = await req.text(); - if (!source) { - return new Response("No source provided", { status: 400 }); + const importValue = await req.text(); + if (!importValue) { + // This request will error and future requests will hang. + return new Response("No source or import value found", { status: 400 }); } - handler = (await import(source)).default; + handler = (await import(importValue)).default; initialized = true; initializing = false; for (const { req, resolve } of pendingRequests) { resolve(handler(req)); } - } catch (e: any) { + } catch (e) { return new Response(e, { status: 500 }); } return new Response(""); diff --git a/tsconfig.json b/tsconfig.json index 468affa..298f581 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,5 +17,5 @@ "rootDir": "./src", "outDir": "./dist" }, - "exclude": ["dist", "deno-bootstrap"] + "exclude": ["dist", "deno-bootstrap", "src/test"] } \ No newline at end of file