Allow to serve raw TS

This commit is contained in:
Michaël Lemaire 2021-07-27 22:17:38 +02:00
parent 1b37c64ef8
commit 45fb0df4b6
8 changed files with 206 additions and 78 deletions

View file

@ -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`

1
config/run.flags Normal file
View file

@ -0,0 +1 @@
--allow-run=deno --allow-net=0.0.0.0,code.thunderk.net

3
doc/about.md Normal file
View file

@ -0,0 +1,3 @@
## About
On-the-fly bundler for typescript libraries hosted on Gitea or compatible

2
doc/index Normal file
View file

@ -0,0 +1,2 @@
about
run

17
doc/run.md Normal file
View file

@ -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`

19
run Executable file
View file

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

View file

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

View file

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