From 45fb0df4b69c5d0d3e74eb51d5ec45b1b08eb75f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Lemaire?= Date: Tue, 27 Jul 2021 22:17:38 +0200 Subject: [PATCH] Allow to serve raw TS --- README.md | 22 ++++++++ config/run.flags | 1 + doc/about.md | 3 + doc/index | 2 + doc/run.md | 17 ++++++ run | 19 +++++++ server.test.ts | 144 +++++++++++++++++++++++++++-------------------- server.ts | 76 +++++++++++++++++++------ 8 files changed, 206 insertions(+), 78 deletions(-) create mode 100644 config/run.flags create mode 100644 doc/about.md create mode 100644 doc/index create mode 100644 doc/run.md create mode 100755 run diff --git a/README.md b/README.md index 5152fc0..5686909 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,25 @@ # typescript/bundler [![Build Status](https://thunderk.visualstudio.com/typescript/_apis/build/status/bundler?branchName=master)](https://dev.azure.com/thunderk/typescript/_build?pipelineNameFilter=bundler) + +## About + +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] +``` + +You can now get raw typescript files (usable by Deno): + +`http://localhost:8000/libname@1.0.0/path/file.ts` + +Or get a bundled JS version: + +`http://localhost:8000/libname@1.0.0/path/file.js` + +If the version is not specified, the master branch is used instead: + +`http://localhost:8000/libname/path/file.js` diff --git a/config/run.flags b/config/run.flags new file mode 100644 index 0000000..93eabbf --- /dev/null +++ b/config/run.flags @@ -0,0 +1 @@ +--allow-run=deno --allow-net=0.0.0.0,code.thunderk.net \ No newline at end of file diff --git a/doc/about.md b/doc/about.md new file mode 100644 index 0000000..c29676e --- /dev/null +++ b/doc/about.md @@ -0,0 +1,3 @@ +## About + +On-the-fly bundler for typescript libraries hosted on Gitea or compatible diff --git a/doc/index b/doc/index new file mode 100644 index 0000000..6b42eac --- /dev/null +++ b/doc/index @@ -0,0 +1,2 @@ +about +run \ No newline at end of file diff --git a/doc/run.md b/doc/run.md new file mode 100644 index 0000000..17c279b --- /dev/null +++ b/doc/run.md @@ -0,0 +1,17 @@ +## How to run + +```shell +deno run --allow-run=deno --allow-net=0.0.0.0,yourgithosting.net server.ts yourgithosting.net/namespace [port] +``` + +You can now get raw typescript files (usable by Deno): + +`http://localhost:8000/libname@1.0.0/path/file.ts` + +Or get a bundled JS version: + +`http://localhost:8000/libname@1.0.0/path/file.js` + +If the version is not specified, the master branch is used instead: + +`http://localhost:8000/libname/path/file.js` diff --git a/run b/run new file mode 100755 index 0000000..74d1c6d --- /dev/null +++ b/run @@ -0,0 +1,19 @@ +#!/bin/sh +# Simplified run tool for deno commands + +if test $# -eq 0 +then + echo "Usage: $0 [file or command]" + exit 1 +elif echo $1 | grep -q '.*.ts' +then + denocmd=run + denoargs=$1 + shift +else + denocmd=$1 + shift +fi + +denoargs="$(cat config/$denocmd.flags 2> /dev/null) $denoargs $@" +exec deno $denocmd $denoargs diff --git a/server.test.ts b/server.test.ts index 6ae28e3..25a69d3 100644 --- a/server.test.ts +++ b/server.test.ts @@ -1,73 +1,95 @@ import { + describe, expect, it, mockfn, -} from "https://code.thunderk.net/typescript/devtools/raw/1.2.2/testing.ts"; -import { processRequest } from "./server.ts"; +} from "https://code.thunderk.net/typescript/devtools/raw/1.3.0/testing.ts"; +import { processRequest, Response } from "./server.ts"; -it("serveBundles standard", async () => { - const mock_run = mockfn(() => { - return { - output: () => Promise.resolve(new TextEncoder().encode("abc")), - status: () => Promise.resolve({ code: 0, success: true }), - } as any; +describe("serveBundles", () => { + it("calls deno bundle if asking for js", async () => { + const run = 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({ + cmd: [ + "deno", + "bundle", + "https://git.example.com/libs/greatlib/raw/1.0.0/reader/file.ts", + ], + stdout: "piped", + }); }); - const response = await processRequest( - { url: "/greatlib/1.0.0/reader/" } as any, - mock_run, - ); - expect(response).toEqual({ body: new TextEncoder().encode("abc") }); - expect(mock_run).toHaveBeenCalledTimes(1); - expect(mock_run).toHaveBeenCalledWith({ - cmd: [ - "deno", - "bundle", - "https://code.thunderk.net/typescript/greatlib/raw/1.0.0/reader.ts", - ], - stdout: "piped", + 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", + }), + }); + expect(run).not.toHaveBeenCalled(); + }); + + it("handles bad method", async () => { + const [response, run] = await call("/greatlib@1.0.0/reader/file.ts", { + method: "POST", + }); + expect(response).toEqual({ status: 405 }); + expect(run).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");', + }, + ); + expect(run).not.toHaveBeenCalled(); + }); + + it("handles bundle failure", async () => { + const run = 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, + }); + 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");', + }, + ); }); }); -it("serveBundles bad path", async () => { - const mock_run = mockfn(() => { - return { - output: () => Promise.resolve(new TextEncoder().encode("abc")), - status: () => Promise.resolve({ code: 0, success: true }), - } as any; - }); - +type CallOptions = { method: string; run: ReturnType }; +async function call( + url: string, + options: Partial = {}, +): Promise<[Response, ReturnType]> { + const method = options.method ?? "GET"; + const run = options.run ?? mockfn(); const response = await processRequest( - { url: "/greatlib/1.0.0/reader{}/" } as any, - mock_run, + { method, url } as any, + "git.example.com/libs", + run, ); - expect(response).toEqual( - { - status: 400, - body: - 'console.error("bundler error - Invalid path https://code.thunderk.net/typescript/greatlib/raw/1.0.0/reader{}.ts");', - }, - ); - expect(mock_run).toHaveBeenCalledTimes(0); -}); - -it("serveBundles bundle fail", async () => { - const mock_run = mockfn(() => { - return { - output: () => Promise.resolve(undefined), - status: () => Promise.resolve({ code: 1, success: false }), - } as any; - }); - - const response = await processRequest( - { url: "/great_lib/1.0.0-dev1/reader/" } as any, - mock_run, - ); - expect(response).toEqual( - { - status: 500, - body: - 'console.error("bundler error - Failed to bundle https://code.thunderk.net/typescript/great_lib/raw/1.0.0-dev1/reader.ts");', - }, - ); -}); + return [response, run]; +} diff --git a/server.ts b/server.ts index fe84fe1..9b94ace 100755 --- a/server.ts +++ b/server.ts @@ -1,30 +1,51 @@ -#!/usr/bin/env -S deno run --allow-run --allow-net +#!./run // Automated bundle server -import { bool } from "https://code.thunderk.net/typescript/functional/raw/1.0.0/all.ts"; import { Response, serve, ServerRequest, -} from "https://deno.land/std@0.79.0/http/server.ts"; +} from "https://deno.land/std@0.103.0/http/server.ts"; export async function processRequest( req: ServerRequest, + hostpath: string, runner = Deno.run, ): Promise { - const params = req.url.split("/").filter(bool); - const lib = params[0] || "all"; - const version = params[1] || "master"; - const file = params.length > 2 ? params.slice(2).join("/") : "all"; - const path = - `https://code.thunderk.net/typescript/${lib}/raw/${version}/${file}.ts`; - if (!path.match(/^[a-z0-9\/\.\-:_]+$/)) { + if (req.method != "GET") { + return { status: 405 }; + } + + const params = req.url.split("/").filter((x) => !!x); + if (params.length < 2) { + return { status: 404 }; + } + + const [lib, version] = params[0].split("@", 2); + const file = params.slice(1).join("/"); + 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}");`, }; } + if (path.endsWith(".js")) { + return await bundle(path.slice(0, -3) + ".ts", runner); + } else { + return { + status: 301, + headers: new Headers({ Location: path }), + }; + } +} + +async function bundle( + path: string, + runner: typeof Deno.run, +): Promise { const process = runner({ cmd: ["deno", "bundle", path], stdout: "piped", @@ -41,16 +62,37 @@ export async function processRequest( } } -export async function serveBundles() { - const listen = { hostname: "0.0.0.0", port: 8000 }; +export async function serveBundles(hostpath: string, port: number) { + const listen = { hostname: "0.0.0.0", port }; const server = serve(listen); - console.log(`Serving bundles on ${listen.hostname}:${listen.port} ...`); + console.log( + `Serving ${hostpath} bundles on ${listen.hostname}:${listen.port} ...`, + ); for await (const req of server) { - const response = await processRequest(req); - await req.respond(response); + try { + const response = await processRequest(req, hostpath); + await req.respond(response); + } catch (err) { + // console.error(err); + await req.respond({ status: 500 }); + } } } -if (import.meta.main) { - await serveBundles(); +function isName(input: string): boolean { + return !!input.match(/^[a-zA-Z0-9_\-\.]+$/); +} + +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]"); + } }