Use Deno http server and avoid redirects

This commit is contained in:
Michaël Lemaire 2021-11-09 19:32:23 +01:00
parent 45fb0df4b6
commit dcd0c212b9
8 changed files with 112 additions and 92 deletions

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
deno.d.ts
.vscode
.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
```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):

11
cli.ts Normal 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
```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):

View file

@ -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<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(
url: string,
path: string,
options: Partial<CallOptions> = {},
): Promise<[Response, ReturnType<typeof mockfn>]> {
): Promise<CallResult> {
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();
}
}

View file

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