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 # typescript/bundler
[![Build Status](https://thunderk.visualstudio.com/typescript/_apis/build/status/bundler?branchName=master)](https://dev.azure.com/thunderk/typescript/_build?pipelineNameFilter=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 { import {
describe,
expect, expect,
it, it,
mockfn, mockfn,
} from "https://code.thunderk.net/typescript/devtools/raw/1.2.2/testing.ts"; } from "https://code.thunderk.net/typescript/devtools/raw/1.3.0/testing.ts";
import { processRequest } from "./server.ts"; import { processRequest, Response } from "./server.ts";
it("serveBundles standard", async () => { describe("serveBundles", () => {
const mock_run = mockfn(() => { it("calls deno bundle if asking for js", async () => {
const run = 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 processRequest(
{ url: "/greatlib/1.0.0/reader/" } as any,
mock_run,
);
expect(response).toEqual({ body: new TextEncoder().encode("abc") }); expect(response).toEqual({ body: new TextEncoder().encode("abc") });
expect(mock_run).toHaveBeenCalledTimes(1); expect(run).toHaveBeenCalledTimes(1);
expect(mock_run).toHaveBeenCalledWith({ expect(run).toHaveBeenCalledWith({
cmd: [ cmd: [
"deno", "deno",
"bundle", "bundle",
"https://code.thunderk.net/typescript/greatlib/raw/1.0.0/reader.ts", "https://git.example.com/libs/greatlib/raw/1.0.0/reader/file.ts",
], ],
stdout: "piped", stdout: "piped",
}); });
});
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;
}); });
const response = await processRequest( it("redirects to raw file if asking for anything other than js", async () => {
{ url: "/greatlib/1.0.0/reader{}/" } as any, const [response, run] = await call("/greatlib@1.0.0/reader/file.ts");
mock_run, 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( expect(response).toEqual(
{ {
status: 400, status: 400,
body: body:
'console.error("bundler error - Invalid path https://code.thunderk.net/typescript/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(mock_run).toHaveBeenCalledTimes(0); expect(run).not.toHaveBeenCalled();
}); });
it("serveBundles bundle fail", async () => { it("handles bundle failure", async () => {
const mock_run = mockfn(() => { const run = 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 processRequest( run,
{ url: "/great_lib/1.0.0-dev1/reader/" } as any, });
mock_run,
);
expect(response).toEqual( expect(response).toEqual(
{ {
status: 500, status: 500,
body: body:
'console.error("bundler error - Failed to bundle https://code.thunderk.net/typescript/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> };
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(
{ method, url } as any,
"git.example.com/libs",
run,
);
return [response, run];
}

View file

@ -1,30 +1,51 @@
#!/usr/bin/env -S deno run --allow-run --allow-net #!./run
// Automated bundle server // Automated bundle server
import { bool } from "https://code.thunderk.net/typescript/functional/raw/1.0.0/all.ts";
import { import {
Response, Response,
serve, serve,
ServerRequest, 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( export async function processRequest(
req: ServerRequest, req: ServerRequest,
hostpath: string,
runner = Deno.run, runner = Deno.run,
): Promise<Response> { ): Promise<Response> {
const params = req.url.split("/").filter(bool); if (req.method != "GET") {
const lib = params[0] || "all"; return { status: 405 };
const version = params[1] || "master"; }
const file = params.length > 2 ? params.slice(2).join("/") : "all";
const path = const params = req.url.split("/").filter((x) => !!x);
`https://code.thunderk.net/typescript/${lib}/raw/${version}/${file}.ts`; if (params.length < 2) {
if (!path.match(/^[a-z0-9\/\.\-:_]+$/)) { 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 { return {
status: 400, status: 400,
body: `console.error("bundler error - Invalid path ${path}");`, 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({ const process = runner({
cmd: ["deno", "bundle", path], cmd: ["deno", "bundle", path],
stdout: "piped", stdout: "piped",
@ -41,16 +62,37 @@ export async function processRequest(
} }
} }
export async function serveBundles() { export async function serveBundles(hostpath: string, port: number) {
const listen = { hostname: "0.0.0.0", port: 8000 }; const listen = { hostname: "0.0.0.0", port };
const server = serve(listen); 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) { for await (const req of server) {
const response = await processRequest(req); try {
const response = await processRequest(req, hostpath);
await req.respond(response); await req.respond(response);
} catch (err) {
// console.error(err);
await req.respond({ status: 500 });
}
} }
} }
if (import.meta.main) { function isName(input: string): boolean {
await serveBundles(); 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]");
}
} }