diff --git a/.gitignore b/.gitignore index d69a4c0..8b7abdc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ deno.d.ts .vscode .local +.output +web/*.js diff --git a/README.md b/README.md index 5686909..91f9f5a 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ On-the-fly bundler for typescript libraries hosted on Gitea or compatible ## How to run ```shell -deno run --allow-run=deno --allow-net=0.0.0.0,yourgithosting.net server.ts yourgithosting.net/namespace [port] +deno run --allow-run=deno --allow-net=0.0.0.0,yourgithosting.net cli.ts yourgithosting.net/namespace [port] ``` You can now get raw typescript files (usable by Deno): diff --git a/cli.ts b/cli.ts new file mode 100644 index 0000000..48fc5f5 --- /dev/null +++ b/cli.ts @@ -0,0 +1,11 @@ +#!./run + +import { serveBundles } from "./server.ts"; + +if (import.meta.main) { + if (Deno.args.length >= 1 && Deno.args.length <= 2) { + await serveBundles(Deno.args[0], parseInt(Deno.args[1] || "8000")); + } else { + console.error("Usage: server.ts yourgithosting.net/namespace [port]"); + } +} diff --git a/deps.testing.ts b/deps.testing.ts new file mode 100644 index 0000000..66adec0 --- /dev/null +++ b/deps.testing.ts @@ -0,0 +1,8 @@ +// WARNING - Do not get deps from a bundler website! + +export { + describe, + expect, + it, + mockfn, +} from "https://code.thunderk.net/typescript/devtools/raw/1.3.0/testing.ts"; diff --git a/deps.ts b/deps.ts new file mode 100644 index 0000000..85468af --- /dev/null +++ b/deps.ts @@ -0,0 +1 @@ +// WARNING - Do not get deps from a bundler website! diff --git a/doc/run.md b/doc/run.md index 17c279b..6c6e19e 100644 --- a/doc/run.md +++ b/doc/run.md @@ -1,7 +1,7 @@ ## How to run ```shell -deno run --allow-run=deno --allow-net=0.0.0.0,yourgithosting.net server.ts yourgithosting.net/namespace [port] +deno run --allow-run=deno --allow-net=0.0.0.0,yourgithosting.net cli.ts yourgithosting.net/namespace [port] ``` You can now get raw typescript files (usable by Deno): diff --git a/server.test.ts b/server.test.ts index 25a69d3..1fbd330 100644 --- a/server.test.ts +++ b/server.test.ts @@ -1,23 +1,20 @@ -import { - describe, - expect, - it, - mockfn, -} from "https://code.thunderk.net/typescript/devtools/raw/1.3.0/testing.ts"; -import { processRequest, Response } from "./server.ts"; +import { describe, expect, it, mockfn } from "./deps.testing.ts"; +import { processRequest } from "./server.ts"; describe("serveBundles", () => { it("calls deno bundle if asking for js", async () => { - const run = mockfn(() => { + const runner = mockfn(() => { return { output: () => Promise.resolve(new TextEncoder().encode("abc")), status: () => Promise.resolve({ code: 0, success: true }), } as any; }); - const [response, _] = await call("/greatlib@1.0.0/reader/file.js", { run }); - expect(response).toEqual({ body: new TextEncoder().encode("abc") }); - expect(run).toHaveBeenCalledTimes(1); - expect(run).toHaveBeenCalledWith({ + const { response } = await call("/greatlib@1.0.0/reader/file.js", { + runner, + }); + await checkResponse(response, 200, "abc"); + expect(runner).toHaveBeenCalledTimes(1); + expect(runner).toHaveBeenCalledWith({ cmd: [ "deno", "bundle", @@ -27,69 +24,87 @@ describe("serveBundles", () => { }); }); - it("redirects to raw file if asking for anything other than js", async () => { - const [response, run] = await call("/greatlib@1.0.0/reader/file.ts"); - expect(response).toEqual({ - status: 301, - headers: new Headers({ - Location: - "https://git.example.com/libs/greatlib/raw/1.0.0/reader/file.ts", - }), + it("serves raw file if asking for anything other than js", async () => { + const fetcher = mockfn(() => new Response("abc")); + const { response, runner } = await call("/greatlib@1.0.0/reader/file.ts", { + fetcher, }); - expect(run).not.toHaveBeenCalled(); + await checkResponse(response, 200, "abc"); + expect(runner).not.toHaveBeenCalled(); + expect(fetcher).toHaveBeenCalledTimes(1); + expect(fetcher).toHaveBeenCalledWith( + "https://git.example.com/libs/greatlib/raw/1.0.0/reader/file.ts", + { redirect: "follow" }, + ); }); it("handles bad method", async () => { - const [response, run] = await call("/greatlib@1.0.0/reader/file.ts", { + const { response, runner } = await call("/greatlib@1.0.0/reader/file.ts", { method: "POST", }); - expect(response).toEqual({ status: 405 }); - expect(run).not.toHaveBeenCalled(); + await checkResponse(response, 405); + expect(runner).not.toHaveBeenCalled(); }); it("handles bad path", async () => { - const [response, run] = await call("/greatlib@1.0.0/reader{}.ts"); - expect(response).toEqual( - { - status: 400, - body: - 'console.error("bundler error - Invalid path https://git.example.com/libs/greatlib/raw/1.0.0/reader{}.ts");', - }, + const { response, runner } = await call("/greatlib@1.0.0/reader{}.ts"); + await checkResponse( + response, + 400, + 'console.error("bundler error - Invalid path https://git.example.com/libs/greatlib/raw/1.0.0/reader{}.ts");', ); - expect(run).not.toHaveBeenCalled(); + expect(runner).not.toHaveBeenCalled(); }); it("handles bundle failure", async () => { - const run = mockfn(() => { + const runner = mockfn(() => { return { output: () => Promise.resolve(undefined), status: () => Promise.resolve({ code: 1, success: false }), } as any; }); - const [response, _] = await call("/great_lib@1.0.0-dev1/reader.js", { - run, + const { response } = await call("/great_lib@1.0.0-dev1/reader.js", { + runner, }); - expect(response).toEqual( - { - status: 500, - body: - 'console.error("bundler error - Failed to bundle https://git.example.com/libs/great_lib/raw/1.0.0-dev1/reader.ts");', - }, + await checkResponse( + response, + 500, + 'console.error("bundler error - Failed to bundle https://git.example.com/libs/great_lib/raw/1.0.0-dev1/reader.ts");', ); }); }); -type CallOptions = { method: string; run: ReturnType }; +type CallContext = { + runner: ReturnType; + fetcher: ReturnType; +}; +type CallOptions = CallContext & { + method: string; +}; +type CallResult = CallContext & { + response: Response; +}; async function call( - url: string, + path: string, options: Partial = {}, -): Promise<[Response, ReturnType]> { +): Promise { const method = options.method ?? "GET"; - const run = options.run ?? mockfn(); + const runner = options.runner ?? mockfn(); + const fetcher = options.fetcher ?? mockfn(); const response = await processRequest( - { method, url } as any, + { method, url: "http://localhost:8000" + path } as any, "git.example.com/libs", - run, + runner, + fetcher, ); - return [response, run]; + return { response, runner, fetcher }; +} + +async function checkResponse(res: Response, status = 200, body?: string) { + expect(res.status).toEqual(status); + if (body) { + expect(await res.text()).toEqual(body); + } else { + expect(res.body).toBeNull(); + } } diff --git a/server.ts b/server.ts index 9b94ace..9ccb9b4 100755 --- a/server.ts +++ b/server.ts @@ -1,24 +1,18 @@ -#!./run // Automated bundle server -import { - Response, - serve, - ServerRequest, -} from "https://deno.land/std@0.103.0/http/server.ts"; - export async function processRequest( - req: ServerRequest, + req: Request, hostpath: string, runner = Deno.run, + fetcher = fetch, ): Promise { if (req.method != "GET") { - return { status: 405 }; + return new Response(undefined, { status: 405 }); } - const params = req.url.split("/").filter((x) => !!x); + const params = req.url.split("/").filter((x) => !!x).slice(2); if (params.length < 2) { - return { status: 404 }; + return new Response(undefined, { status: 404 }); } const [lib, version] = params[0].split("@", 2); @@ -26,19 +20,16 @@ export async function processRequest( const branch = version || "master"; const path = `https://${hostpath}/${lib}/raw/${branch}/${file}`; if (!isName(lib) || !isName(branch) || !isPath(file)) { - return { - status: 400, - body: `console.error("bundler error - Invalid path ${path}");`, - }; + return new Response( + `console.error("bundler error - Invalid path ${path}");`, + { status: 400 }, + ); } if (path.endsWith(".js")) { return await bundle(path.slice(0, -3) + ".ts", runner); } else { - return { - status: 301, - headers: new Headers({ Location: path }), - }; + return await fetcher(path, { redirect: "follow" }); } } @@ -53,28 +44,30 @@ async function bundle( const output = await process.output(); const status = await process.status(); if (status.success) { - return { body: output }; + return new Response(output); } else { - return { - status: 500, - body: `console.error("bundler error - Failed to bundle ${path}");`, - }; + return new Response( + `console.error("bundler error - Failed to bundle ${path}");`, + { status: 500 }, + ); } } export async function serveBundles(hostpath: string, port: number) { const listen = { hostname: "0.0.0.0", port }; - const server = serve(listen); console.log( `Serving ${hostpath} bundles on ${listen.hostname}:${listen.port} ...`, ); - for await (const req of server) { - try { - const response = await processRequest(req, hostpath); - await req.respond(response); - } catch (err) { - // console.error(err); - await req.respond({ status: 500 }); + + for await (const conn of Deno.listen(listen)) { + for await (const req of Deno.serveHttp(conn)) { + try { + const response = await processRequest(req.request, hostpath); + await req.respondWith(response); + } catch (err) { + // console.error(err); + await req.respondWith(Response.error()); + } } } } @@ -86,13 +79,3 @@ function isName(input: string): boolean { function isPath(input: string): boolean { return !input.split("/").some((part) => !isName(part)); } - -export type { Response }; - -if (import.meta.main) { - if (Deno.args.length >= 1 && Deno.args.length <= 2) { - await serveBundles(Deno.args[0], parseInt(Deno.args[1] || "8000")); - } else { - console.error("Usage: server.ts yourgithosting.net/namespace [port]"); - } -}