Use Deno http server and avoid redirects
This commit is contained in:
parent
45fb0df4b6
commit
dcd0c212b9
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
||||||
deno.d.ts
|
deno.d.ts
|
||||||
.vscode
|
.vscode
|
||||||
.local
|
.local
|
||||||
|
.output
|
||||||
|
web/*.js
|
||||||
|
|
|
@ -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
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,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):
|
||||||
|
|
111
server.test.ts
111
server.test.ts
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
67
server.ts
67
server.ts
|
@ -1,24 +1,18 @@
|
||||||
#!./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 != "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 +20,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 +44,30 @@ 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);
|
||||||
} 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 +79,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]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue