Compare commits

...

2 commits

8 changed files with 124 additions and 92 deletions

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
deno.d.ts deno.d.ts
.vscode .vscode
.local .local
.output
web/*.js

View file

@ -9,7 +9,7 @@ On-the-fly bundler for typescript libraries hosted on Gitea or compatible
## How to run ## How to run
```shell ```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): You can now get raw typescript files (usable by Deno):

11
cli.ts Executable file
View file

@ -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]");
}
}

8
deps.testing.ts Normal file
View file

@ -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";

1
deps.ts Normal file
View file

@ -0,0 +1 @@
// WARNING - Do not get deps from a bundler website!

View file

@ -1,7 +1,7 @@
## How to run ## How to run
```shell ```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): You can now get raw typescript files (usable by Deno):

View file

@ -1,23 +1,20 @@
import { import { describe, expect, it, mockfn } from "./deps.testing.ts";
describe, import { processRequest } from "./server.ts";
expect,
it,
mockfn,
} from "https://code.thunderk.net/typescript/devtools/raw/1.3.0/testing.ts";
import { processRequest, Response } from "./server.ts";
describe("serveBundles", () => { describe("serveBundles", () => {
it("calls deno bundle if asking for js", async () => { it("calls deno bundle if asking for js", async () => {
const run = mockfn(() => { const runner = mockfn(() => {
return { return {
output: () => Promise.resolve(new TextEncoder().encode("abc")), output: () => Promise.resolve(new TextEncoder().encode("abc")),
status: () => Promise.resolve({ code: 0, success: true }), status: () => Promise.resolve({ code: 0, success: true }),
} as any; } as any;
}); });
const [response, _] = await call("/greatlib@1.0.0/reader/file.js", { run }); const { response } = await call("/greatlib@1.0.0/reader/file.js", {
expect(response).toEqual({ body: new TextEncoder().encode("abc") }); runner,
expect(run).toHaveBeenCalledTimes(1); });
expect(run).toHaveBeenCalledWith({ await checkResponse(response, 200, "abc");
expect(runner).toHaveBeenCalledTimes(1);
expect(runner).toHaveBeenCalledWith({
cmd: [ cmd: [
"deno", "deno",
"bundle", "bundle",
@ -27,69 +24,87 @@ describe("serveBundles", () => {
}); });
}); });
it("redirects to raw file if asking for anything other than js", async () => { it("serves raw file if asking for anything other than js", async () => {
const [response, run] = await call("/greatlib@1.0.0/reader/file.ts"); const fetcher = mockfn(() => new Response("abc"));
expect(response).toEqual({ const { response, runner } = await call("/greatlib@1.0.0/reader/file.ts", {
status: 301, fetcher,
headers: new Headers({
Location:
"https://git.example.com/libs/greatlib/raw/1.0.0/reader/file.ts",
}),
}); });
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 () => { 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", method: "POST",
}); });
expect(response).toEqual({ status: 405 }); await checkResponse(response, 405);
expect(run).not.toHaveBeenCalled(); expect(runner).not.toHaveBeenCalled();
}); });
it("handles bad path", async () => { it("handles bad path", async () => {
const [response, run] = await call("/greatlib@1.0.0/reader{}.ts"); const { response, runner } = await call("/greatlib@1.0.0/reader{}.ts");
expect(response).toEqual( await checkResponse(
{ response,
status: 400, 400,
body: 'console.error("bundler error - Invalid path https://git.example.com/libs/greatlib/raw/1.0.0/reader{}.ts");',
'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 () => { it("handles bundle failure", async () => {
const run = mockfn(() => { const runner = mockfn(() => {
return { return {
output: () => Promise.resolve(undefined), output: () => Promise.resolve(undefined),
status: () => Promise.resolve({ code: 1, success: false }), status: () => Promise.resolve({ code: 1, success: false }),
} as any; } as any;
}); });
const [response, _] = await call("/great_lib@1.0.0-dev1/reader.js", { const { response } = await call("/great_lib@1.0.0-dev1/reader.js", {
run, runner,
}); });
expect(response).toEqual( await checkResponse(
{ response,
status: 500, 500,
body: 'console.error("bundler error - Failed to bundle https://git.example.com/libs/great_lib/raw/1.0.0-dev1/reader.ts");',
'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<typeof mockfn> }; type CallContext = {
runner: ReturnType<typeof mockfn>;
fetcher: ReturnType<typeof mockfn>;
};
type CallOptions = CallContext & {
method: string;
};
type CallResult = CallContext & {
response: Response;
};
async function call( async function call(
url: string, path: string,
options: Partial<CallOptions> = {}, options: Partial<CallOptions> = {},
): Promise<[Response, ReturnType<typeof mockfn>]> { ): Promise<CallResult> {
const method = options.method ?? "GET"; const method = options.method ?? "GET";
const run = options.run ?? mockfn(); const runner = options.runner ?? mockfn();
const fetcher = options.fetcher ?? mockfn();
const response = await processRequest( const response = await processRequest(
{ method, url } as any, { method, url: "http://localhost:8000" + path } as any,
"git.example.com/libs", "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();
}
} }

79
server.ts Executable file → Normal file
View file

@ -1,24 +1,25 @@
#!./run
// Automated bundle server // Automated bundle server
import {
Response,
serve,
ServerRequest,
} from "https://deno.land/std@0.103.0/http/server.ts";
export async function processRequest( export async function processRequest(
req: ServerRequest, req: Request,
hostpath: string, hostpath: string,
runner = Deno.run, runner = Deno.run,
fetcher = fetch,
): Promise<Response> { ): Promise<Response> {
if (req.method == "OPTIONS") {
return new Response(undefined, {
headers: {
"Access-Control-Allow-Origin": "*",
},
});
}
if (req.method != "GET") { 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) { if (params.length < 2) {
return { status: 404 }; return new Response(undefined, { status: 404 });
} }
const [lib, version] = params[0].split("@", 2); const [lib, version] = params[0].split("@", 2);
@ -26,19 +27,16 @@ export async function processRequest(
const branch = version || "master"; const branch = version || "master";
const path = `https://${hostpath}/${lib}/raw/${branch}/${file}`; const path = `https://${hostpath}/${lib}/raw/${branch}/${file}`;
if (!isName(lib) || !isName(branch) || !isPath(file)) { if (!isName(lib) || !isName(branch) || !isPath(file)) {
return { return new Response(
status: 400, `console.error("bundler error - Invalid path ${path}");`,
body: `console.error("bundler error - Invalid path ${path}");`, { status: 400 },
}; );
} }
if (path.endsWith(".js")) { if (path.endsWith(".js")) {
return await bundle(path.slice(0, -3) + ".ts", runner); return await bundle(path.slice(0, -3) + ".ts", runner);
} else { } else {
return { return await fetcher(path, { redirect: "follow" });
status: 301,
headers: new Headers({ Location: path }),
};
} }
} }
@ -53,28 +51,35 @@ async function bundle(
const output = await process.output(); const output = await process.output();
const status = await process.status(); const status = await process.status();
if (status.success) { if (status.success) {
return { body: output }; return new Response(output, {
headers: {
"Content-Type": "text/javascript",
"Access-Control-Allow-Origin": "*",
},
});
} else { } else {
return { return new Response(
status: 500, `console.error("bundler error - Failed to bundle ${path}");`,
body: `console.error("bundler error - Failed to bundle ${path}");`, { status: 500 },
}; );
} }
} }
export async function serveBundles(hostpath: string, port: number) { export async function serveBundles(hostpath: string, port: number) {
const listen = { hostname: "0.0.0.0", port }; const listen = { hostname: "0.0.0.0", port };
const server = serve(listen);
console.log( console.log(
`Serving ${hostpath} bundles on ${listen.hostname}:${listen.port} ...`, `Serving ${hostpath} bundles on ${listen.hostname}:${listen.port} ...`,
); );
for await (const req of server) {
try { for await (const conn of Deno.listen(listen)) {
const response = await processRequest(req, hostpath); for await (const req of Deno.serveHttp(conn)) {
await req.respond(response); try {
} catch (err) { const response = await processRequest(req.request, hostpath);
// console.error(err); await req.respondWith(response);
await req.respond({ status: 500 }); } catch (err) {
console.error(err);
await req.respondWith(Response.error());
}
} }
} }
} }
@ -86,13 +91,3 @@ function isName(input: string): boolean {
function isPath(input: string): boolean { function isPath(input: string): boolean {
return !input.split("/").some((part) => !isName(part)); 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]");
}
}