Use Deno http server and avoid redirects
This commit is contained in:
parent
45fb0df4b6
commit
dcd0c212b9
8 changed files with 112 additions and 92 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
|||
deno.d.ts
|
||||
.vscode
|
||||
.local
|
||||
.output
|
||||
web/*.js
|
||||
|
|
|
@ -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
11
cli.ts
Normal 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
8
deps.testing.ts
Normal 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
1
deps.ts
Normal file
|
@ -0,0 +1 @@
|
|||
// WARNING - Do not get deps from a bundler website!
|
|
@ -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):
|
||||
|
|
111
server.test.ts
111
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<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();
|
||||
}
|
||||
}
|
||||
|
|
67
server.ts
67
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<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]");
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue